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に必要な構文はさすがに苦しいと思う。

くらいの意見があります。