こちらの投稿を見て、以前自分も Crystal で同じようなことをやってみたのを思い出したので投稿します。
「静的型付き言語を使おうが、文字列型は変わらずバグの温床である」は Crystal のマクロなら少し改善できるというお話です。
やりたいこと
「あるパターンにマッチする文字列」を表す型を作りたい。例えば、URL型なら、URLのフォーマットを持つ文字列のみを保持できる型を作りたい。とりわけ、
- 正しいフォーマットの文字列リテラルが渡された場合は非
Nil
なURL
インスタンスを返す - 誤ったフォーマットが渡された場合はコンパイルエラーを返す
- 変数が渡された場合はコンパイル時のチェックは不可能なので実行時にチェックを行う。そのため戻り値の型は
URL | Nil
となる。
つまり以下のようなことが実現できるようにしたい。
URL.parse("http://www.google.com") #=> URL
URL.parse("www.google.com") #=> compile error
s = "http://www.google.com"
URL.parse(s) #=> URL | Nil
コード
macro を使えば一発です。
class URL
REGEX = /http(s)?:/
def initialize(url)
raise "invalid pattern" unless url =~ REGEX
@url = url
end
macro parse(str)
{% if str.is_a? StringLiteral %}
{% if str =~ REGEX %}
URL.new({{str}})
{% else %}
{% raise "invalid URL pattern" %}
{% end %}
{% else %}
({{str}} =~ URL::REGEX ? URL.new({{str}}) : nil)
{% end %}
end
end
簡単に解説すると、文字列リテラルが渡された場合に限りマクロの中でパターンにマッチするかどうか正規表現でチェックしてもしマッチしなければ例外を投げるだけです。
実際コンパイルエラーを出してみると以下のようになります。
Error in url.cr:25: invalid URL pattern
url = URL.parse("www.google.com")
^~~~~
まとめ
このように Crystal ではコンパイル時に簡単な計算を走らせることでコード中のリテラルのチェックを行うことができます。実際のコードで使ったことはまだありません。電話番号やJSONなど他のフォーマットに応用は可能だと思います(簡単かどうかは別として)。
このようなコンパイル時の能力の高さが Crystal に惹かれた理由の一つです。(他にもこんなことができる言語ってあるんでしょうか?)