LoginSignup
3
1

More than 5 years have passed since last update.

Crystalのジェネリクスで簡単な型制約をかける

Last updated at Posted at 2017-04-04

Crystal言語にはジェネリクスの仕組みがありますが,型変数に対する制約をかける構文は,v0.21.1時点ではまだ用意されていないようです1

Ary Borenszweig氏などは,ジェネリクスを単純なコンテナ以外の目的で使用することについては消極的なよう2ですが,Scalaと同じまでとは言わないまでも,特定のクラスの子孫だけを受け入れるジェネリクスがあると良いな,と思うことは間々あります。

ジェネリクスの定義時点では型変数は何型でもありませんが,ジェネリクスを使用する時点ではその型変数が何型なのかがわかっているはずです。また,型変数はジェネリクス型の定義の中であれば通常の型と同じように使用できますので,ある型が他の型の子孫であるかどうかを判定できれば,オブジェクトの生成時に型チェックをして例外を投げることはできそうな気がします。

ところが,RubyだとSomeClass < OtherClassSomeClassOtherClassを継承しているかどうかを調べられるのですが,CrystalのClass型には#<#>などは用意されていません。

Int32 < Number
#=> Error in line 1: undefined method '<' for Int32:Class

さてどうしたものかと思ったのですが,Crystalコンパイラや標準添付ライブラリのソースを眺めていた時にそれっぽい記述を見たような薄っすらとした記憶があったため諸々調べてみると,マクロが使用するTypeNode3にはRubyのような#>#<などのインスタンスメソッドが用意されており,これらを使って型の継承関係をチェックすることができるようです。

という訳で,N型が先祖にNumberを持っていない場合には,マクロを使って#initialize中に例外を吐くコードを埋め込むという強引な方法ですが,数値だけを入れられる箱NumBox型が実現できました。(何気に,型変数N{{N}}で展開すると,実行時点の型名になってくれています)

numbox.cr
class NumBox(N)
  class InvalidType < Exception; end

  getter value

  def initialize(@value : N)
    {% unless N < Number %}
      raise InvalidType.new("{{N}} is not a Number type.")
    {% end %}
  end

  def to_s(io)
    io << @value
  end

end

puts NumBox(Int32).new(1)
puts NumBox(Float64).new(1.0)
puts NumBox(String).new("1")
実行結果
$ crystal run numbox.cr 
1
1.0
String is not a Number type. (NumBox::InvalidType)
0x1073e3f52: *CallStack::unwind:Array(Pointer(Void)) at ??
0x1073e3ef1: *CallStack#initialize:Array(Pointer(Void)) at ??
0x1073e3ec8: *CallStack::new:CallStack at ??
0x1073e3221: *raise<NumBox::InvalidType>:NoReturn at ??
0x107410eef: *NumBox(String)@NumBox(N)#initialize<String>:NoReturn at ??
0x107410ecc: *NumBox(String)@NumBox(N)::new<String>:NumBox(String) at ??
0x1073ddb2e: __crystal_main at ??
0x1073e3068: main at ??

#<#>以外にも,マクロのTypeNode型には,その型がユニオン型かどうかを判定する#union?や,インスタンス変数やメッソドのリストを返すメソッドなども用意されていて,使い様によってはかなり強力な道具になりそうです。

追記

実行時の例外よりもコンパイル時にエラーになって欲しいよね,ということで,マクロでコンパイル時エラーが出せるっぽいです。(#initializeの中でエラーを出しちゃったせいで,エラー箇所の表示が変なことになっちゃってますが)

numbox2.cr
class NumBox(N)
  class InvalidType < Exception; end

  getter value

  def initialize(@value : N)
    {% unless N < Number %}
      {% raise "#{N} is not a Number type." %}
    {% end %}
  end

  def to_s(io)
    io << @value
  end

end

puts NumBox(Int32).new(1)
puts NumBox(Float64).new(1.0)
puts NumBox(String).new("1")
実行結果
$ crystal run numbox2.cr 
Error in test.cr:20: String is not a Number type.

puts NumBox(String).new("1")
                    ^~~
3
1
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
3
1