LoginSignup
1
0

More than 5 years have passed since last update.

文字列型のバグをできるだけ埋め込みたくない(Crystal編)

Last updated at Posted at 2017-08-28

こちらの投稿を見て、以前自分も Crystal で同じようなことをやってみたのを思い出したので投稿します。

「静的型付き言語を使おうが、文字列型は変わらずバグの温床である」は Crystal のマクロなら少し改善できるというお話です。

やりたいこと

「あるパターンにマッチする文字列」を表す型を作りたい。例えば、URL型なら、URLのフォーマットを持つ文字列のみを保持できる型を作りたい。とりわけ、

  • 正しいフォーマットの文字列リテラルが渡された場合は非 NilURL インスタンスを返す
  • 誤ったフォーマットが渡された場合はコンパイルエラーを返す
  • 変数が渡された場合はコンパイル時のチェックは不可能なので実行時にチェックを行う。そのため戻り値の型は 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 に惹かれた理由の一つです。(他にもこんなことができる言語ってあるんでしょうか?)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0