こんにちは!ふーが です。
あっという間に 12 月ですね。12 月といえばアドベントカレンダー!
ということでこの記事は ESM Advent Calendar 2022 の 6 日目の記事です!
この記事の目的
RBS が Ruby 本体にバンドルされるようになってからまもなく 2 年となります。 「型導入を試してみた」という Web 記事はいくつか見かけるのですが、実際の運用例についてはまだあまり語られていません。
私が関わるプロジェクトでは半年ほど前に型の導入を始め、運用してきました。 その導入方法や運用方針についてお伝えすることで、「そんな感じでできるんだ!」というのを感じていただいて、型を導入して運用してみよう!というプロジェクトが増えればうれしいなと思いこの記事を書きます。
なお、この記事の内容は Kaigi on Rails 2022 で私が登壇した際の内容を多分に含んでいます。
ご覧いただいた方にとっては、目新しい内容はあまりないのでその点お含み置きの上ご覧ください。
Railsプロジェクトへの型導入
導入の目的
弊プロジェクトでは「開発者体験の向上」を軸として型導入を進めています。
steep check
コマンドで型チェックをすると(導入初期は特に)エラーが多く発生しますが、エラーの解消よりも型定義を追加することに注力しています。
この目的が運用方針にも大きく関わってくるため、メンバー間で認識を合わせておくとよさそうです。
使用ツール
プロジェクトに型を導入するにあたりいくつかのツールを利用しています。 ほぼ必須のツールとなるためここで簡単に紹介をしておきます。
RBS
Ruby プログラムに型定義をするための言語です。 具体的な記述方法は rbs/rbs_by_example.md や rbs/syntax.md を参考にすると良いでしょう。
RBS とは別に RBI という gem もあります。 こちらも同じく型定義をできるようにするための gem なのですが、RBS は
- Ruby 本体にバンドルされていること
- プロダクトコードと別ファイルで型定義を管理できること
といった理由から RBS を使うことにしました。
Steep
Ruby の静的型解析のための gem です。
型解析を実行するための steep check
や型定義ファイルの構文チェックをする steep validate
コマンドをよく使います。
前述の rbi を使用する場合、型解析には Sorbet が使えます。
rbs_rails
Rails 向けに rbs ファイルを生成してくれる gem です。
プロジェクトの Gemfile に含めて bundle install
した上でコマンドを実行すると、プロダクトコードに対する rbs ファイルが生成されます。
gem_rbs_collection
サードパーティ製 gem の型定義を集約しているリポジトリです。 ここから gem の型定義ファイルをインストールすることで、プロジェクト内で使用している gem の型情報を得ることができます。
導入手順
初手としては概ね次のとおりになると思います。
- Steep の導入
- rbs_rails による型定義の生成
- gem_rbs_collection から gem の型定義をインストール
- プロダクトコードに対する型定義
- RBS を使用した自動生成 + 修正
- 手作業による型定義
こちらの具体的な手順については解説記事が多くあるため割愛します。
弊プロジェクトに導入する際は、RBS のメンテナである pocke さんによる RBS Railsを使ってRailsアプリケーションにSteepを導入する - pockestrap を参考にさせていただきました。
(弊プロジェクトでは記事に出てくる rbs prototype
コマンドを使用して型定義をガッと自動生成しました)
また、rbs prototype
コマンドで自動生成した場合はほとんどが untyped
として生成されるため、手作業での型定義(修正)は必須です。
導入後のほとんどの作業はこの修正になるかと思います。
ただし、導入の前に チーム内での合意を得る ことが大変なのではないかと思います。 ここでは合意を得るにあたって有用そうなことをいくつか挙げてみたいと思います。 私がチームメンバーに説明したときは、以下に挙げる内容を伝えました。
小さく少しずつ進められる
rbs ファイルはプロダクトコードからは独立しておりランタイムに影響を与えませんし、導入段階からすべての型定義が揃っている必要もありません。 主要なモデルや API だけ型定義をする、という選択もできるため導入を試しやすいです。
実際弊プロジェクトでも時間を見つけて少しずつ型定義を増やしたり、調整したりしています。
不必要と判断したらすぐに剥がせる
上記のとおり rbs ファイルは独立しているため、導入してみたけど「やっぱりやめたい」となった場合でも型定義ファイルを配置しているディレクトリ(通常 sig/
と .gem_rbs_collection/
)を削除して、Gemfile からツール類を削除すればすっかり元通りにすることができます。
もちろん削除によって既存のコードに影響を与えることはありません。
運用方法
Rails プロジェクトにおける型の運用はまだベストプラクティスというものがなく手探りで進めていくことになります。 ここでは弊プロジェクトでの型の運用における考え方や方針などを書いていきます。
型定義の進め方
導入時点でジェネレーターを使用したかどうかに関わらず、型の定義や修正がメインの作業になってきます。 入力補完やコードジャンプなどの恩恵を受けやすく型定義しやすい部分ということで、基本的にはよく使うモデルから型定義を進めていくのがベターかと思います。
また、型定義を進める中で正しく型付けするのがどうしても難しいケースがあります。
そういった時にはひとまず untyped
としてしまうのもありだと思います。
完璧な型定義が理想ではありますが、それ以上に広範に型定義を進めていくことが大事だという考え方もできます。
これは先ほど紹介した gem_rbs_collection の CONTRIBUTING.md でも語られている考え方です。
Focus on the most important part of the API You may want to write everything of a gem. We don't recommend doing it especially when you are starting. Writing high-quality type definitions are difficult. Focus on examples available through the README or docs of the gem. Focus on the APIs your app is using.
gem_rbs_collection/CONTRIBUTING.md at main · ruby/gem_rbs_collection · GitHub
レビュー方針
型定義を main(master) ブランチにマージしていくにあたっての弊プロジェクトでの方針は大きく 3 つあります。
型は書ける人が書く
弊プロジェクトは社内でも大きめのチームで人数も多く、全員が型を書くことにモチベーションを持っているわけではありません。 導入自体が experimental な中で型を書くことを義務化してしまうと、"やらされ感" からのモチベーション低下や生産性、開発速度の低下は避けられないと思います。
型によって得られるメリットよりデメリットが上回ってしまっては本末転倒なので、書ける人だけが型を書くようにしてそうでない人は型について気にしなくて良い形にしています。
機能追加の PR とは別の独立した PR にする
新しくモデルやメソッドを追加したり、既存のメソッドに修正が入った場合には rbs ファイルの修正も必要になります。 通常、こういったケースではプロダクトコードの変更と rbs ファイルの変更を同一の PR として開きたくなりますが、あえてわけるようにしています。
前述のとおり弊プロジェクトではメンバー全員が型に関わらなくても良い方針にしています。 そんな中で PR に rbs ファイルの変更が含まれているとノイズになってしまうからです。 レビューについても見られる人が見れば良い、というスタンスにしています。
レビューなしでマージしてもかまわない
見られる人が見れば良い、というスタンスで進めていると、タスクの状況によってはレビューする人がいないというケースもあり得ます。 そういう場合にはノーレビューでマージしても OK ということにしています。
rbs ファイルはランタイムに影響を与えるものではなく、仮に rbs に誤りやプロダクトコードとの乖離があったとしてもアプリケーションに悪影響はないからです。 正確性を担保するためにきっちりレビューをしてもらうのはもちろん良いことなのですが、今のプロジェクトの現状に照らすと正確性を重視するよりもガンガン型定義を追加していく方針の方が前進しそうということでこういった形を取っています。
CIに組み込むか?
結論からいうと CI には組み込んでいません。
steep check
, rbs validate
などのコマンドで型定義に対するテストを CI に組み込み、正しい型定義のみをリポジトリに取り込みたいというのはそうなのですが、現状だとこれは現実的ではないと考えて CI には組み込まないことにしています。
理由は 2 つあります。
エラーや警告をなくすことは現状だと難しい
gem の型定義が足りていなかったり、型解析自体にまだ不完全な部分があるなど、ハード面でエラーをなくすことが難しいというのが一つです。 そういう状況である以上、CI に組み込めば RED になり続けることは避けられません。 GREEN にならない CI は精神衛生上良くないですし、何より "オオカミ少年化" してしまって本来対応しなければいけないはずの検知を見逃してしまう原因にもなりかねません。
CI に組み込むことによるこのデメリットは非常に大きいものだと思い、組み込まない選択をしました。
一方で、API の構造が変わったり新しいクラスを追加した場合に型定義との乖離が起こっていることは、CI なしで気づくのが大変です。 ここはトレードオフになるかと思います。 弊プロジェクトでは乖離が起こっても気づいた時に修正すれば良い、というスタンスで CI に組み込まないメリットの方を優先しています。
目的はエラーの解消ではなく"開発者体験の向上"
冒頭でも書いたとおり、目的は "開発者体験の向上" であってエラーの解消ではありません。 CI で検知するのは型定義の誤りや漏れなわけですが、そこを検知して対応することよりも、新しく型定義を追加して全体としてのカバー率を上げていく方を優先したいと考えました。
全体的な型定義を充実させた上で型定義の正確性やエラーの解消に着手していく方が、"開発者体験の向上" という目的に適っていると考えたのも理由の一つです。
おわりに
Rails プロジェクトへの完璧な型定義は「簡単、手軽に」というわけにはいかないのが実情ではありますが、導入については気軽に始められるものだとこの半年の運用を通じて感じています。 また、限定的にではあるものの「型があることによる恩恵」を受けられている実感もあります。
型の導入をやってみたい、型に興味がある、という方はぜひ "年末のお型付け" としてプロジェクトへの型導入にチャレンジしてみてはいかがでしょうか?
すでに導入している方やこれから始めるという方、ぜひお話しして情報交換しましょう✌️
なお冒頭に書いた登壇時の資料はこちらですのでよろしければあわせてご覧ください。
永和システムマネジメントでは Ruby やアジャイルを使ってより良い社会をともに目指していく仲間を募集しております。