Google Summer of Code 2020: 最終レポート
この夏、自分はGoogle Summer of Code (GSoC) 2020に参加しました。
このポストはその最終レポートで、Googleに提出した英語記事 の和訳です。
公式サイト上の、自分のプロジェクトページはこちら: Brush up RBS and related tools for practical Rails apps
やったこと (TL;DR)
- RBSと関連ツールを実践的なRailsアプリに適用
- Ruby CIの作業ブランチ
- MastodonのUserモデルの作業ブランチ
- 型チェックによってアプリ側のバグを発見
- ruby/rubyci#196
- Mastodonにはデットコードがありそう
- ツール群の改善・バグ修正
- RBSの新しいサブコマンド
rbs subtract
をほぼ実装
背景
プロジェクトページの説明に書いた通り、RBSはRuby 3.0の一部としてリリースされようとしています。
RBSは型注釈の言語というだけでなく、それを操作するツールの名前でもあります。RBSは、SquareのリードRubyエンジニアである松本宗太郎さんによって活発に開発されていています。RBS導入に良い記事として、宗太郎さんご自身によるThe State of Ruby 3 Typing があります(ただし英語)。
RBSによって記述される型の情報によって、Rubyistは静的解析の恩恵を受ける事ができます。最近のPythonやTypeScriptも好きなRubyユーザーは、歓迎するでしょう。
ですが、RBSはまだ開発中であり、すぐに実用になるかどうかはやや不透明でした。特にRubyの主なユースケースとして、現実のRailsアプリの型チェックで役立つかどうかは重要な点です。
このお話には、他にも登場人物がいます。RBS RailsとSteepです。
RBS Railsは、Pockeこと桑原仁雄さんによってメンテされています。RBSとRailsをいい感じに繋げるツールで、例えばRails関連のRBSのシグネチャ(*.rbs
) を自動生成してくれます。
またSteepは、RBSと同じく松本宗太郎さんによってメンテされていて、RBSシグネチャを元にしたRuby実装 (*.rb
) のチェックを実際に行う型チェッカーです。
これで役者が揃いました。RBS、RBS Rails、Steepです。今回の自分のプロジェクトではこれら3つのツールについて、実用性の検証・改善を試みました。
詳細
この夏、自分が行った作業は、だいたい3つに分けられます。
RBS関連ツールを現実のRailsアプリに適用
以下2つの実用Railsアプリに対して、前述の3ツールを適用しました。
- Ruby CI (型付けした作業ブランチ)
- Mastodon、特にUserモデル (〃)
まずRuby CIは、Rubyの中央リポジトリのテスト結果を定期的にチェック・表示するWebサイトです。小さく、かつ馴染みのあるサイトなので、今回の題材として選びました。
もう一つのMastodonは、Twitterのようなマイクロブログサービスで、セルフホストができる物です。サイズが逆に大きく、他の現実のサービスと同じようによく使われているため、題材としました。
自分はこれら2つのアプリの *.rbs
ファイルを書きましたが、Railsアプリとしての共通部分はRBS Railsが自動生成してくれました。またRBSを書きつつ、Steepによる静的解析を試しました。
全体としてはほぼうまく動き、(Mastodonのような)大きなモデルクラスにも有用でした。小さい問題は断続的に見つかりましたが、次の「改善」で書くように、いずれも既に修正済みか報告済みです。またそういった問題による、作業全体への深刻な影響はありませんでした。
これらによって、Railsアプリ開発の場面でも、RBSと関連する既存ツールが十分効果的・実用的であると示せたと思います。
ただ残念ながら、主にチェックできたのはモデルクラスのみで、大きなコードベースを持つMastodonについては一つのモデルしか検証できていません。他の部分、コントローラー・ビュー・ヘルパーなどへの適用は、将来の課題になります。
バグの発見
Steepによる型チェックは、実装側のクラスをほぼ正しいと判定しましたが、このチェックによって2つのバグがアプリ側に見つかりました。
Ruby CIには500エラーになるバグがありました。これは nil
チェックが足りなかったためで、自分のPRによって既に修正済みです。
MastodonのUserモデルには reset_password!
メソッドがありますが、これはどうもデッドコードで、依存するgemを含めて他の場所からは呼ばれていないように見えました。これはSteepが、「このメソッド内のsuper
がNoMethodError
を起こすよ」と教えてくれたために気づいた物です。たしかに、(Mastodonが利用する)Deviseは、同名のメソッドを4年前に廃止しているようです。しかし更に注意深いチェックが必要そうなため、その部分を削除するPRは今後の課題です。
RBS関連ツールの様々な改善
ツールたちをRailsアプリに適用している間に、たくさんの問題が見つかりました。それらはすべて、上流リポジトリに報告済みです。
- RBS
- ruby/rbs#294 ドキュメントのバグ修正
- ruby/rbs#299
require_relative
を正しく使うように振る舞いを変更 - ruby/rbs#300 古い名前のお掃除
- ruby/rbs#301 無名モジュールに関連した、プロトタイプファイル生成での問題をIssueとして登録 (#302で修正)
- ruby/rbs#302 無名クラス・モジュールの問題を追加で修正
- RBS Rails
- pocke/rbs_rails#12 対応するAR内のレコードタイプを追加
- pocke/rbs_rails#28
inet
型サポートの追加 - pocke/rbs_rails#29
AR::Relation#last
のシグネチャ追記 - pocke/rbs_rails#30
Rails.env
のシグネチャ追記 - pocke/rbs_rails#32 ARが自動的に定義するメソッドのシグネチャを追加
- Steep
- soutaro/steep#158 単発での型チェックの問題をIssue登録 (#171で修正)
- soutaro/steep#173
steep watch
終了の高速化
これらの報告はすべて、現実のRailsアプリでの適用実験に基づいた物なので、改善によって更にRBSが実用的になったと考えています。
RBSの新しいサブコマンド rbs subtract
の実装(未完)
プロジェクトの最後に、新しいRBSコマンドの機能 rbs subtract
を実装しようとしました。残念ながらこれは完成しておらず、マージもまだですが、これによってRBSを伴うRuby開発のワークフローが改善すると思っています。
どうして必要?
まず、ゼロからRBSシグネチャを書くのは大変です。そのためRBSは rbs prototype
コマンドを提供していて、プロトタイプとなるファイルを生成する事ができます。
実装 → プロトタイプのRBS
ですがこのコマンドは現在、RBSを書き始める最初に使われる事を意図していて、ファイルの差分を検知する機能はありません。また簡単に作れるとはいえ、自動生成はいつも完全とは限りません。多くの場合、手動修正が必要になります。
実装 → プロトタイプのRBS → 修正済みRBS
さて、開発が進めば実装ファイルが変化します。RBSファイル側にも新しく変更が必要になりますが、これには先程のプロトタイプ生成を使う事ができます。
実装 → プロトタイプのRBS → 修正済みRBS (B) \→ 新しい実装 → 新しいプロトタイプのRBS (A)
ここで問題が起こります。開発者は、新しいRBS (ここでは A
とします) を生成できますが、先程も書いた通り、これには手動修正が必要になります。一方、修正済みのRBS (B
とします) は既に古くなっています。
つまりこのままだと、開発者は実装を変更をする度に、A
と B
の手動マージを毎回、行わなければいけません。
しかし現実には、B
に比べて新しい A
にクラスやメソッドが増える事が多いでしょう。理想的には、この増えた差分だけが手動修正の対象になるべきですし、残りの既存部分にはBをそのまま使えるはずです。
そういうわけで、 A - B
が必要になります。ここで rbs subtract
の出番です。
rbs subtract
は文字通り、シグネチャを引き算するコマンドです。引数としてAに対応するファイルを渡すと、既存のシグネチャには存在せずAのみに存在するメソッド・定数などを出力します。実際の作業の流れは、こういう感じになるでしょう。
- 最初のプロトタイプ
sig/app.rbs
を生成した後、修正して正しい型を埋める$ rbs prototype rb app.rb >sig/app.rbs
- 実装ファイル
app.rb
が更新される - 不完全だけれど新しい実装に対応するファイル
app-new.rbs
を生成$ rbs prototype rb app.rb > app-new.rbs
- 差分を取るために引き算・追記
$ rbs subtract app-new.rbs >> sig/app.rbs
- これで後は、
sig/app.rbs
に追加された差分だけを修正すればOK!
このようにして、実装に追加された差分のみを自動生成できれば、開発中にRBSシグネチャを手書きするコストはかなり下がります。
逆に、実装の内容が減ったケースについては将来の課題です。しかしこの場合、必要なチェックがなくなったり、間違ったチェックがされたりはしないため、実際の不都合は小さい物だと考えています。
実装の詳細
これが進行中の作業ブランチです。まず、新旧シグネチャ間のASTレベルでの重複を除去するため、 RBS::AST::DuplicationFilter
を追加しています。また、新しいCLIのサブコマンドなので、 cli.rb
中に run_subtract
メソッドを追加し、その中で先程の DuplicationFilter
を使っています。
一番の難所はASTのすべてのパターンをカバーするところだと思いますが、その部分はほぼできています。
この機能全体を実現するため、もう少しだけがんばる予定です。
結論
RBSツール類を、RubyCIとMastodonのような実用Railsアプリに適用しました。RBSの実用性を示しただけでなく、実際のバグを実装(アプリ)側に見つけました。
この実験を元にして、RBSと関連ツールに複数の改善と修正を行いました。RBSは十分、実用的なレベルに達していると思います。
また新しいRBSのサブコマンド rbs subtract
の実装をほぼ行いました。これが実現すれば、開発のワークフローはより効率的になると思います。完成と上流へのマージは将来のタスクです。
謝辞
まずは自分のメンター、遠藤侑介さんに感謝します。Rubyの深い知識があり、型プロファイラの実装者でもある遠藤さんと作業できたのは、とても心強い事でした。
また、松本宗太郎さんと桑原仁雄さん(Pockeさん)の継続的なサポートにも感謝します。この4人での毎週のZoomミーティングは本当にありがたい存在で、特にお二人のRBS・Steep・RBS Railsのメンテナとしてのアドバイスは有用でした。また笹田耕一さんを始め、様々なコメントを下さったRubyコミッター・Rubyistの皆さんにも感謝しています。
加えて、Ruby OrganizationとしてGSoCに関係して下さった皆さん、特に全体を取りまとめていたHiren Mistry氏に感謝します。週に一度の英語Zoomミーティングでは、たくさんのポジティブなアドバイスと、時々訪れるゲストからの励ましの言葉をもらえました。英会話がまったく得意でない自分は最初、緊張しましたが、Hirenさんや他のメンターの方々はいつも親切で優しく、前向きな雰囲気が保たれているミーティングでした。
最後に、Google Summer of Codeを主催して下さったGoogle社、特に運営に携わった方々に感謝します。フルタイムの担当メンバーがわずか数名の状況で、他の複数のプロジェクトも掛け持ちする中、世界中に散らばった学生とやり取りを続けるのはとても大変だったと想像します。社会貢献でもあるGSoCのようなプロジェクトが、今後も継続される事を願っています。