Sorbetについて
昨日のAsakusa.rbに行ったらSorbetについて聞かれたので、少し話したんだけど、ちょっときちんと確認しないまま話したらぐだぐだだったので、もう少しまとめておく。
全体的な話として、Sorbetはとても良くできているので、これで良いと思う人はそれで良いと思います。
$ srb --version Sorbet typechecker 0.4.4306 git 31ef906fe5eb572865f2b5d3ea5805793554c676 built on 2019-06-25 06:45:18 GMT with debug symbols
Rubyと少し合わないところはあるので、その辺を挙げる。
1. サブタイピング
いわゆるDuck typingというやつが、まあ良くない。
sig { params(arg: Eachable).void } def push_from(arg) arg.each do |x| @array << x end end
みたいなの。この引数のEachable
が特定のクラスなら良いんだけど、each
が定義された自作のクラスを渡したりArray
を渡したりするとちょっと難しいことになる。
なるんだけど、workaroundは用意されているので、それで良い人はそれで良いと思う。
# .rbiに欲しいメソッドが定義されたモジュールを定義する module Eachable sig { params(blk: T.proc.params(x: String).void).void } def each; end end # .rbにモジュールを定義する(このファイルは型検査しない) # typed: false module Eachable # これはincludeできればいいので空で良い end # 別に.rbにクラスなどを定義する class A # このincludeによってAのインスタンスはEachableになった include Eachable def each(&blk) yield "foo" yield "bar" yield "baz" end end push_from A.new # これがokになる
動かしてないんだけど、これでいけるはず。
「既存のクラスについても後からinclude
することでEachable
にできる」というのがポイントなんだけど、Array
でやろうとしたら型が難しかったので諦めた……
2. 型の構文
好みの問題なので、これが良いと思う人も世の中にはいるのかもしれない。
結構強烈なのは
def initialize(x:) @x = T.let(x, String) end
のような「T.let
メソッドでインスタンス変数の型を書く」だと思う。
3. Overloading
できないみたい。Sorbetの.rbi
には複数のsig
を一つのdef
に対して付けることができないみたいだ。
Array#map
とかはどうなっているのかというと、標準ライブラリは特別扱いになっているようにみ見える。 # typed: __STDLIB_INTERNAL
などと書いてある。
で、これだと困るような気がするのは
def fetch_people if block_given? yield people.get(id) else people.get(id) end end
みたいな「ブロックがあったらyield
して返り値を返す」パターンで、これ僕は結構書くので、ちょっと苦しいと思う。
4. Runtime
ライブラリに使おうとすると、Rubyコードの実行に言語内DSLのためのランタイムが必要なので、まあそれどうなのという。
ランタイムがあることの利点はあって、ランタイムの型検査ができるようになるし、Typed Structsみたいな便利ライブラリもあるので、なかなか難しいところだと思う。
感想
僕は、
- 絶対にRuntimeの挙動を変えたくないし、gemにはRuntimeの依存関係を増やしたくない
- 型の構文が苦しい(何日かすれば慣れるかもしれないけど)
- Overloadingが全然書けないのはさすがに問題では
- 実はStructural subtypingにこだわるつもりはないんだけど、今のworkaroundに必要な構文はさすがに苦しいと思う。
くらいの意見があります。