春ですね。先程、子供の保育園での最終登園を終え、万感の思いです。
システム基盤部の栗原(ksss)です。
この度、開発しているプロダクトのコードにRubyの型チェック機能を導入しました。
私が導入までに工夫したことを紹介したいと思います。これを読んでいる皆様のプロダクトにおけるRBS導入の参考になれば幸いです。
プロダクトとエコシステムをWin-Winに
社内の開発者にRubyの型に興味を持ってもらい、使ってもらうことができれば、フィードバックを得ることができます。 このフィードバックから開発体制を改善し、ひいてはツールの不具合修正や機能改善を提案することによって、Rubyエコシステム全体に貢献できます。 そうすれば、さらに開発体制が良くなる好循環が生まれると目論んでいます。
つまり最初の興味が大事なのです。
より興味を持ってもらうことを目的に、社内でRBSハンズオンを開催しました。
RBSハンズオン
READYFORのエンジニアリング本部では、毎週バックエンドチャプター定例として、開発者が当番制で好きなトピックで30分ほど発表する場を設けています。
この場をかりて、RBSハンズオンを開催してみました。仕組みに感謝。
簡単にいろいろ試せる遊び場として、以下のリポジトリを用意しました。(publicなので誰でも遊べます)
https://github.com/ksss/steep-playground
開発者にこのリポジトリをcloneしてきてもらい、実際に自分のエディタで動作を確かめてもらうことで興味を持ってもらおうという目論見でした。
私もこのリポジトリを開いて画面をキャストしつつ、オートコンプリート機能やホバーによる型情報の表示、型のミスマッチによるエラー表示なんかをデモしました。
上々な反応を頂いたので、さらに手元で準備していた、アプリケーションコードに型チェック用の環境を整えた画面をキャストして見せることで、普段見ているアプリケーションコードを通して興味を持ってもらう二段構えでのハンズオンとなりました。
社内の開発者からのフィードバックとしては、
- 「逆に特定の型がついているコードを一覧できたら、リファクタリングに役立ちそう。」
- 「型を手で書くのはメンテナンスも大変そうなので、できればやりたくない。」
- 「警告ノイズが多いのはつらい。」
のような有益な感想をいただき、今後のRBS導入への工夫に繋がりました。
ドキュメント
せっかく興味を持ってもらっても、暗黙知が多かったり分からないことが多かったりすると非常にもったいないです。
どうやって使うのか、なぜ導入したのか、なぜこういう導入の仕方だったのか等をドキュメントにまとめておくことで、この問題に対応しました。実際のドキュメントは入社すると読めますが、以下の四点を大事な指標としました。
- エディタ支援を主眼とする。
- CIチェックとかはやらないよ。
- 正しさより便利さ。
- あくまで便利ツールとして使うよ。
- できるだけ自動生成に頼る。
- 型はできるだけ手では書かず、rbs_rails等の自動生成ツールを多用するよ。
- フィードバックしよう。
- フィードバックから開発環境がよくなるよ。OSSチャンスもいっぱいあるよ。
簡単な質問も気軽にできるように、「質問などは、このslackチャンネルでお気軽にどうぞ!みんなで改善していきましょう!」というようなメッセージも添えています。
実際に導入
rbs collection
RBSではライブラリーの型定義を別のリポジトリから取り込む機能をもっています。しかもGemfile.lockを見て自動的に取り込むライブラリーを決めてくれます。便利。
rbs collectionを導入してrbs_collection.yaml
とrbs_collection.lock.yaml
をcommitしておき、後に掲載するrake taskでinstallできるようにしておきます。
ref: https://github.com/ruby/rbs/blob/master/docs/collection.md
rbs_rails
rbs_railsも導入します。Railsがモデル定義時に自動的につくるメソッドに対して型定義を生成してくれます。例えばカラム名のメソッドやscope、enumなんかにも対応しています。私もActiveStorage対応や、has_secure_password対応などの機能追加を行いフィードバックしています。
自動補完
rbs_railsで基本的なモデルの型を生成できますが、アプリケーションに書かれた定数やメソッドは自分でsignatureを書く必要があります。
しかしながら「型を手で書くのは面倒」というフィードバックから、ここも自動化してみました。実際にはRBSのコードであるRBS::Prototype::RB
やRBS::Writer
をカスタムして、classやmodelのsignatureや、シンプルな値を持つ定数は自動的にsignatureを生成するようにしました。カスタムによって、untyped
を含むsignatureはsignature自体を出力しないように調整しました。出力先はsig/auto_completion
としました。
この辺は試行錯誤中なのでより良い方法を探していきたいと思います。
patch
READYFORは歴史あるサービスなので、様々なgemに依存しています。よってこれまでの自動生成によってでも、signatureがまだないgem名を継承したりincludeしたりしていると、どうしても未定義なsignatureが出てきます。
これらはsig/patch
ディレクトリを切って、gem別に空の定義を書くことにしました。例えば
sig/patch/activestorage.rbsは
module ActiveStorage module Downloading end end
のような感じです。
さらにこのディレクトリは、ドキュメントでコントリビューションチャンスであることを強調しています。 定義が足りないならみんなで書こうという方針をドキュメントにも記載しました。
gitignore
rbs collectionのinstall先になるディレクトリ.gem_rbs_collection
や、sig/rbs_rails
とsig/auto_completion
は共にgitignoreすることで、自動生成であることを強調しつつ、コード検索の邪魔にならないようしました。
rake task
これまでをまとめて、以下のようなrake taskにしてみました。(プロダクトに関係ないコードです)
gist46d7bdd3ddf8c5595d8ac4d58b057397
これまでのことを何も覚えていなくても$ bundle exec rake rbs:setup
とするだけで、全部の準備が整うようにしています。
導入してから
導入しただけで終わりではなく、ノイズ低減のためのエラー情報をYAMLファイルに退避したり、利用のサポート、ツールの改善等をやっていき、より良い開発体験を目指していきたいと思います。