Sorbet
https://sorbet.org
Stripe社が公開しているOSS
Rubyに漸進的型付け gradual typingを可能にする
2つの大きなコンポーネントで構成される
sorbet gem
コードの静的解析を行うCLIツール
srb tc
実行時には必要ない
sorbet-runtime gem
Rubyのコードにtype annotationを加えられるようにする
T namespace と sig methodによるsignatureの記述
実行時に動的に型検査を行う
ohbarye.icon runtimeの挙動を変えるというのはsteepとの最大の違いに見える
https://soutaro.hatenablog.com/entry/2019/06/26/145305
2021年7月、Sorbet CompilerがOSSとして発表された
https://sorbet.org/blog/2021/07/30/open-sourcing-sorbet-compiler
コンパイラがLLVM用の中間言語を吐いて、LLVMがnative shared object (*.so)を生成する
shared objectはnative extensionとして使われる
Rubyを使ってRubyのnative extensionが書ける
Rubyを書いているだけで高速化の恩恵が受けられる
漸進的型付け
すでに存在している巨大なコードベースをrewriteするのは現実的ではない
Rubyにはものすごい量の資産がある
TypeScriptはJavaScriptのスーパーセットとなることで漸進的型付けを可能にしている
妥協点として、メソッド呼び出し、メソッド定義、クラス、ファイル単位で型検査をオプトアウトできるようにした
T.untyped
Sorbetの型システムにおける型の一つ
すべての値が T.untyped 型であることを主張することができ、T.untyped型のすべての値は他の型であることを主張することができる
型付けを始めたタイミングではほぼすべてがuntyped
シグネチャを持つメソッドの内部でuntypedとして扱われた値も戻り値がシグネチャ通りにキャストして解釈される
漸進的型付けに短期的には役立つが長期的にはなくしていくべき型
sigil
sigilはファイルの先頭に記述するマジックコメントのこと
sigilによりsrb tcの型検査のレポート内容が変わる
# typed: ignore
Sorbetからはファイルが読まれない
Stripeではignoreしているファイルは、ない!
# typed: false
syntax、定数名の解決、sigの正しさのみレポートされる
sigは記述しなくてもよい
sigilを記述しない場合のデフォルトレベル
# typed: true
type errorがレポートされる
存在しないメソッド呼び出し、メソッドの不正な呼び出し(引数間違い)、型とマッチしない変数の使い方
Stripeではもうすぐ99%がこのレベル https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet
# typed: strict
sigの記述がすべてのメソッドに必要
定数、変数は明示的な型注釈が必要
TypeScriptのnoImplicitAnyに類似
# typed: strong
T.untypedを許容しない
ファイル内のすべてのメソッド呼び出しについて静的に型を決定できている
Stripeでもほぼ使われない
RBIファイルか、ほぼ空のファイルぐらい
Sorbetが機能を足すとT.untypedがもたらされることがあるのでstrongのサポートは最小限にとどめる
実行時検査
sigによりシグネチャが付与されたメソッド呼び出しはラップされ、前後で以下を検査される
宣言にマッチする引数で呼び出されたか
結果が宣言された戻り値にマッチするか
sigが間違っていたら
リファクタリング対象の検出はできない。到達できるコードに到達できないとSorbetは判断してしまうことがある
古いドキュメントと同じ。コメントよりちょっとマシな程度
Rubyの高速化には使えない
将来的には型情報を利用した高速化も検討されているが、sigで嘘をつくと実際には遅くなる
なぜruntime checkをするか
型アノテーションに信頼が増す
自動テストは型アノテーションのテストになる
モニタリングにより間違った仮定がコードベース全体に伝播する前に、悪いsigを早期に検出できる
型を付けたコードを型のないコードから守り、型の採用を促進する
実行時の挙動を変える方法
.on_failure設定
sorbet-runtimeのT::Configuration moduleを使ってType error発生時にはロギングするだけ、など
個別のsigに定義するsig {params(argv: T::Array[String]).void.on_failure(:log)}
.checked
検査するかどうかを選べる
sig {params(argv: T::Array[String]).void.checked(:never)}
型定義ファイル
インラインで型を書くこともできるがRBIファイル.rbiとして記述できる
Rubyファイル.rbと全く同じように記述し、解析できる
メソッドの中身が空なだけ
Cで書かれたライブラリにも型を与えるためにもこの仕様は必要
すべてのライブラリが型注釈を書くのは現実的ではないので https://github.com/sorbet/sorbet-typed に型を置くことができる
DefinitelyTypedと同じ
Sorbetは型注釈が好きな人にも嫌いな人にも価値を提供する
Ruby3との関連
Ruby3 or Sorbetではない
SorbetはRuby coreの上に構築してきたし、これからもそうする方針
Sorbetのsyntaxは100% Rubyの構文
RBSの生成にはSorbetのRBIが使われるはず
本体が型定義のRBSを持つことでSorbetにもメリットがある
現在、Sorbetが標準ライブラリの型定義を持っているおかげで、Sorbetを使い始めた瞬間に25%のメソッド呼び出しがすでにtype coverageになる
https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet
型付けの練習
Playground https://sorbet.run/
srb tc -aにより推論したシグネチャを自動付与し、生成された定義を見てみる
OSSでtypingのcontributionを求めているプロジェクトがあるので試す
https://github.com/Vonage/vonage-ruby-sdk/issues/178
Sorbet本体でも型が足りないので足せる
https://github.com/sorbet/sorbet/issues/2390
Contribution
C++とRubyで書かれており、.rbiやテストを足すのはRubyで書ける
Bazelでビルド、テストを行う
ohbarye.icon https://github.com/sorbet/sorbet/pull/3521
参考
型なし言語のための型
2021-05 SorbetでRailsアプリの型シグネチャ作成とメンテを行ってみた(翻訳)
2023-04 Why I Stopped Using Sorbet in All My Ruby Projects
2024-10 Type on Rails - Railsアプリケーションの安全性と開発体験を型で革新する