Rubyの静的型検査を利用した開発手法の現状まとめ
Leaner 開発チームの黒曜(@kokuyouwind)です。
先週はBurikaigi2022で登壇させていただきました!
今回はリモート開催でしたが、来年は現地でブリしゃぶを食べられると良いですね!
Burikaigi ではRBSから始める静的型付け生活と題して、 Ruby の静的型検査の概要や開発手順などについて登壇しました。
入門的な内容ではありますが、今回は発表内容を大まかにまとめた記事をお届けします。
なお記事の構成上、発表内容から一部割愛している話がありますがご容赦ください。[1]
Ruby 静的型検査の概要
TypeScript の台頭や Python 3.5 での型注釈記法の導入など、動的型付けの言語においても静的型検査の仕組みを導入する動きが近年活発です。
Ruby においても 2020 年 12 月にリリースされた Ruby 3.0.0 において RBS と TypeProf が同梱されるようになり、静的型検査の仕組みが導入されました。
静的型検査のスタンス
静的型検査においては型システムの設計や型記法を文法にどう取り入れるかの判断が重要で、これには各言語個別の事情が絡んできます。
例えば JavaScript は実行系が各ブラウザに乗っている都合上文法の大きな変更が難しかったことから、 TypeScript という静的型を持つ別言語で記述し JavaScript へトランスパイルするという方法が主流になったと推測できます。一方 Python ではコメントの延長線として型注釈記法のみが導入されたうえで、型検査自体は mypy という非標準ライブラリに委ねられています。
Ruby の静的型検査のスタンスは Python に近いですが、より前方互換性を重視したものです。
Ruby の生みの親である Matz は RubyKaigi 2016 において「Ruby は生まれた時から動的型付け言語で,型指定が無くてもこれまで動いているため,型指定はむしろ積極的に外すべきだ」と主張しています。[2]
このため Ruby コード中の型注釈は言語として導入しない方針になっており、型推論や別ファイルでのインターフェイス記述などを中心に設計されています。
静的型検査の構成
Ruby では型注釈記法を導入しない代わりに以下の構成で静的型検査を実現しています。
-
RBS: Ruby のための型記述言語
- .rb ファイルの型情報を .rbs という別ファイルに記述する
-
Steep: 静的型検査器
- RBS を元に Ruby コードの型エラーを検査する
-
TypeProf: 静的型解析機
- Ruby コードを元に型を推論して RBS を生成する
それぞれの関係は下図のようになります。
より詳しい説明については TypeProf 作者である @mametter さんの書かれた以下の記事が参考になります。
静的型検査を用いた開発の手順
静的型検査を開発に活かす場合、大まかには以下の二種類の手法が存在します。
- RBS をすべて記述し、 Steep で静的型検査を実施する
- RBS を記述せず、 TypeProf によって Ruby コードから推論させて型エラーを検知する
実際には TypeProf で RBS の雛形を生成してから Steep で型検査をしたり、 TypeProf の推論が甘い箇所だけ RBS を補助的に記述したりなどの折衷案的な方法も存在しますが、基本方針はいずれかに大別できるでしょう。
RBS をすべて記述し、 Steep で静的型検査を実施する
RBS を記述する場合は、 Steep によって静的型付け言語と同レベルの型安全性が得られ、記述した型定義をドキュメントやライブラリ利用者側での型検査にも利用可能なことがメリットです。
一方で型を記述する手間がかかること、メソッド追加などの動的な処理に対応しづらいことがデメリットといえます。
この方法で開発する際、 VSCode の Steep 拡張 を利用することで型エラー箇所のハイライト表示やコード補完などの開発補助を得ることができます。
例えば IntStack
クラスの push
メソッドを RBS にのみインターフェイス定義し実装しなかった場合は、以下のようにエラーがハイライト表示されます。
以下に Steep での型検査デモを行ったリポジトリがあります。
RBS を記述せず、 TypeProf によって Ruby コードから推論させて型エラーを検知する
RBS を記述せず型推論に頼る場合は、既存の開発と同じ感覚でコードを書いていくだけで、簡単な型エラーの検知や IDE による補完など型の恩恵を得られることがメリットです。
一方で RBS を記述する場合と比べて検知できる型エラーが限られること、ドキュメントとしての型記述を残せないことがデメリットといえます。
こちらも VSCode 拡張の TypeProf for IDE を利用することで IDE による開発補助を得ることが出来ます。
以下は IntStack
クラスを定義し利用コードを書いた例ですが、 push
と pop
の両メソッドについて推論された型情報がインライン表示されています。
以下に TypeProf での型検査デモを行ったリポジトリがあります。
ライブラリ利用時の静的型検査
外部 Gem を使った場合、その外部 Gem の型情報がないと静的型検査が正しく行えません。[3]
コミュニティによって保守されている RBS は gem_rbs_collection リポジトリに置かれており、ここに存在するものであれば型情報を利用できます。それ以外については必要であれば自分で型情報を記述するか、型検査を緩和するかを選択することになります。
型情報の依存性解決
型情報が gem_rbs_collection リポジトリに存在する場合でも、型検査のためにはそれをローカルに取得する必要があります。
これを行うためのツールが Ruby 3.1 で導入された rbs collection
コマンドです。
このコマンドを利用することで Gemfile から依存する Gem を解析し、必要な RBS ファイルをローカルに取得できます。また Steep
や TypeProf
などの利用時も依存性を自動で解決してくれるようになります。
以下に rbs collection
での型検査デモを行ったリポジトリがあります。
なお rbs collection
については製作者の@pockeさんが RubyKaigi Takeout 2021 で開発経緯・利用法などを発表しているため、詳しくはこちらを見ると良いでしょう。
Ruby on Rails 利用時の静的型検査
Ruby on Rails を用いる場合、動的に生成されるメソッドが多いため RBS の記述量が非常に多くなってしまいます。
この場合は rbs_rails を利用してモデルやパスヘルパーの RBS を自動生成することで、作業をある程度減らすことが可能です。
rbs collection
と rbs_rails を組み合わせて型検査してみた記事を以前書いたため、こちらも合わせてご覧ください。
まとめ
Ruby の静的型検査は型情報を別ファイルに記述するもので、型推論の仕組みも合わせてかなり独特なものになっています。この記事では現状を駆け足で確認し、使えるツール類などについても紹介しました。
RBS を書いて Steep で型検査をおこなうにしろ、 TypeProf での型推論のみに任せるにしろ、 実際に試してみると VSCode 拡張を用いた開発サポートはかなり実用的になっていると感じました。
両者については、外部に公開する Gem やクリティカルなビジネスロジックのユースケースでは RBS を記述して厳密に型検査し、 Rails 開発など動的な要素が多く厳密な型検査をそこまで必要ない分野では TypeProf に任せるなどの使い分けが考えられるでしょうか。
Ruby 3.1 で rbs collection
コマンドが導入されるなど改善が続いている分野でもあるので、開発に取り入れつつ今後も注視していきたいですね。
宣伝
Leaner Technologies では静的型検査に興味のあるエンジニアを募集しています!
-
やや脇道にそれた Sorbet についての言及と、記載がややこしくなるジェネリクスなどについての記述をカットしています。 ↩︎
-
RubyKaigi 2016レポートより抜粋。 ↩︎
-
TypeProf で推論する場合はその限りではありませんが、 Gem の内部の型まで都度推論するのは余計な負荷がかかるため、型情報があるに越したことはないでしょう。 ↩︎
Discussion