こんにちは。
名刺アプリ「Eight」でエンジニアをしている鳥山(@pvcresin)です。
先日、スポーツサンダルで炎天下を散歩したのですが、日焼けで足の甲がシマウマみたいになってしまいました。
足にも日焼け止め塗った方が良いかもしれません。
さて今回は、RSpecのテストコードを元にRubyのメソッドの型(sig)を自動生成した話をしたいと思います。
※ 本記事はOmotesando.rb#112での発表資料を元に加筆修正したものです。
目次
背景
Eightでは昨年末にSorbetによるRubyへの型導入を始めました。 導入の経緯や型に関する用語の解説はこちらの記事をご覧ください。
SorbetではRBIというRubyのDSL1で型を付けていきます。 メソッドの型は、RBIの一部であるsig関数を使って次のように書けます。
sig { params(x: Integer).returns(String) } def foo(x) x.to_s end
このコードだけ見ると、型付け作業は一見簡単そうに見えますが、既存コードにゼロから正しい型を書いていくのはなかなか骨の折れる作業です。 そこで、sigを自動生成する方法を模索していました。
当初検討した方法
導入当初に考えていた、sigを自動生成する方法は2つです。
Sorbetのsigサジェスト機能
Sorbetにはsigサジェスト機能があり、型チェックのレベルをstrict以上に設定すると、簡単な型であれば推論して提案してくれます。
ただし、あまり深くまで型を推論してくれないため、呼び出したメソッドに型が当たっていないとすぐにuntypedになってしまいます。 そのため、型があまり書かれていない状態の既存プロジェクトでは活用するのが難しいと感じました。
Gelauto
Gelautoはコードを実行する際に、実際に渡ってきた値から型情報を収集し、sigを挿入してくれるツールです2。
これにより、RSpecやMinitestのテストを実行してsigを自動生成できます。
実際に渡ってきた値を使うため、前述したsigサジェスト機能と比較するとより多くのメソッドに型が当たりますが、複雑な型はuntypedになってしまいます。
また、2019年から存在していますが、直近はあまりアクティブに開発されていない様子でした。
上記のような理由から、どちらの方法も採用を見送っていました。
RubyKaigiでの追い風
そのような状況の中で参加したRubyKaigi 2025では、型に関連する多数の発表がありました。 特に次の2つのトピックが個人的に刺さりました。
コード実行時に型情報を収集してRBSコメントを挿入してくれる、RBS::Traceの登場
RBS::Traceを使うと、# @rbs () -> void
のような@rbsキーワード始まりのRBSコメントがコード内に挿入されるため、
RBS::Inlineを使って型定義ファイル(.rbs)を生成することでSteepなどの型チェックに活用できます。
RSpecやMinitestのサポート、TracePointによる型情報の収集など、Gelautoと類似する点が多くあると感じました。
一方で、いくつかのRailsアプリケーションでテストされており、Gelautoよりも生成される型の精度が高い印象でした。
SorbetがRBSコメントによる型付けを実験的にサポート
Sorbetが#: () -> void
のようなコロン始まりのRBSコメントから型を直接認識するようになった話でした。
さらに、型ユーティリティツールのSpoomに、RBIとRBSコメントを相互に変換できるような機能が追加されました。
コロン始まりのRBSコメントは、別途型定義ファイルを用意せずとも、Sorbet・TypeProfが直接認識可能であり、Steepも将来的には直接認識する予定なので、将来的にはこの形式に統一されていくと感じました。
これらの発表を図にまとめると、このようになります。
RubyKaigi終了後
RubyKaigiが終わった後、私はRBS::Traceでコロン始まりの記法に対応するPRを作成しました3。 そのPRは無事にマージされ、Sorbetを使っているコードベースでもRBS::Traceが適用可能になりました。
Eightでメソッドの型をsigで書き続けている理由
SorbetプロジェクトでRBSコメントを使ってメソッドの型を書けるようになったわけですが、Eightでは今もsigを使用しています。 その理由は2つあります。
1つ目は、SorbetにおけるRBSコメントは実験的なサポートであり、足りない機能が存在するためです。 現在も機能追加やバグ修正が定期的に行われている状態で、RBSコメントを使った場合だとSorbetの持つ機能を最大限活用できないという問題があります。 また、公式ドキュメントでも「実験的なサポートのため、Sorbet開発者へのFeedback目的での使用を推奨」と書かれています。4
2つ目は、エンジニア組織がsigを含むRBI構文にようやく慣れてきたタイミングであり、ここに追加でRBS構文を導入すると混乱が生じると考えたためです。 Sorbetで主に扱う型定義ファイルはRBIのため、仮にコード内がRBSコメントに移行できたとしてもRBIと付き合っていく必要があります。 また、RBSコメントで型を書いたメソッドをエディタ上でホバーした際も、RBI形式に変換された形で表示されるため、両方の文法や対応関係を理解している必要があります。
これらは現状を鑑みた判断であり、今後の動向によって変わる可能性があります。
採用した方法
これまでの状況を踏まえ、sigを自動生成するために、RBS::TraceとSpoomを組み合わせることにしました。
手順は次の通りです。
- RSpecを実行し、RBS::Traceを使ってコロン始まりのRBSコメントを挿入
- Spoomを用いてコロン始まりのRBSコメントをsigに変換
- 静的(Static) / 動的(Runtime)型チェックを行って型を手直し
この作業は、約2日で終わりました。
結果
sigの自動生成を実施した結果、型に関する割合は次のように変化しました。5
before | after | |
---|---|---|
sigが書かれたメソッド | 8% | 32% |
型がついたメソッドの呼び出し | 47% | 59% |
全体的に、型が当たっている部分の割合がかなり向上しました🎉
また、型情報の充実にともない、AIが生成するコードの精度も上がった印象です。
とはいえ、直接的にテストされていないメソッドもあるため、数値的にはまだまだ伸び代があります。
まとめ
今回は、RBS::TraceとSpoomを組み合わせることで、テストからsigを自動生成した話をしました。
テストカバレッジが高いプロジェクトで型を導入する際には、積極的にこういった手法をとると良さそうです。
これからも型を使った品質・開発者体験の向上に努めていきたいと思います。
最後に、EightではRubyが好きなエンジニアを募集中です。 興味をお持ちの方はぜひお気軽にカジュアル面談へ!
- domain-specific language、ドメイン固有言語↩
- 以前書いたGelautoの紹介記事 https://zenn.dev/pvcresin/articles/3f541187aefa39↩
- RubyKaigi 2025事後勉強会の発表資料 https://speakerdeck.com/sansantech/20250516↩
- 公式ドキュメント https://sorbet.org/docs/rbs-support↩
- この数値もSpoomの一機能を使って計測しています↩