なんか色々がちゃがちゃやっていて面白いので、メモしておく。
rbs_rails とは
Rails で RBS (Rubyの型定義ファイル)を扱うためのライブラリ。
「rbs_rails に Rails の型を取り込む」とは
1つは、ユーザーの書いたRailsアプリケーションのコードから、型定義ファイルを生成すること。
例えば、usersテーブルにaccountというString型のカラムがあったら、Userクラスの型にaccountというStringを返すインスタンスメソッドを定義する。
こちらの機能は今回の記事には関係ない。
もう1つの機能は、Rails 自体の型定義を提供すること。
例えば Rails のコードを読み込むとActiveRecord::Baseクラスが定義されるが、それを型として使うにはActiveRecord::Baseを RBS でも定義する必要がある。
そのため、rbs_rails は Rails 自体の型定義を提供している。
この Rails 自体の型定義を提供するにあたって、これを手書きしていたら時間がいくらあっても足りない。 そのため、Rails のソースコードから型定義を自動生成した上で、それを手直しして提供している。
この記事ではその Rails 自体の型定義をどう自動生成しているかについて書く。
生成手順
以下の手順で生成している。
rbs prototypeコマンドで RBS ファイルを生成- 型パラメータを埋めるコマンドを実行
- 1で生成した RBS ファイルに Syntax Error などがあるとこの処理がコケるので、適宜手で直す
ActiveSupport::ConcernによるClassMethodsモジュールをextendする処理を実行rbs validateを実行して、エラーになったところを直す。
以下に各 step を解説する。
rbs prototypeコマンドで RBS ファイルを生成
rbsコマンドが提供するrbs prototypeサブコマンドを使うと、RBS ファイルのプロトタイプが生成できる。
Ruby プログラムから RBS を生成するにはrbs prototype runtimeとrbs prototype rbの2種類があり、rbs_rails では後者を採用している。
runtimeの方は対象のプログラムを読み込み動的に解析するためメタプログラミングによって生成されたメソッドなどにも対応できるのが利点だが、動的に動かすのは面倒なので諦めた。
なお、rbs_rails ではrbs prototype rbをそのまま使うのではなく、手を加えたものを使っている。
これはActiveSupport::Concernのためである。
extend ActiveSupport::Concernしたモジュールではclass_methodsメソッドが有効になり、このメソッドに渡したブロックはClassMethodsクラスの本文として実行される。
つまり以下の2つのコードは等価である。
module M extend ActiveSupport::Concern class_methods do def foo end end end
module M extend ActiveSupport::Concern class ClassMethods def foo end end end
ただしrbs prototype rbはclass_methodsを解釈しないため、このケースではfooは単なるMのインスタンスメソッドとして定義されてしまう。
これを防ぐため、rbs prototype rbを拡張して使用している。
これは次のように使う
# Active Record の型を生成する例 $ bin/rbs-prototype-rb.rb prototype rb /path/to/rails/activerecord/lib/**/*.rb > assets/sig/generated/activerecord.rbs
ここまでで対象のライブラリの型が生成される。 ただし、このままだと読み込んで使える RBS になっていないので、手を入れる必要がある。
型パラメータを埋めるコマンドを実行
まずは、型パラメータを埋める必要がある。
Arrayのように型パラメータを持つクラスを継承したクラスがある場合、rbs prototype rbでは次のような RBS が生成される。
# .rb ファイル class A < Array end
# .rbs ファイル class A < Array end
これはうまく動かない。なぜならばAraryは要素の型を型パラメータとして受け取る必要があるためである。
これを解決するために、bin/add-type-params.rbを書いた。
このプログラムは必要な型パラメータを挿入する。
たとえば上記の .rbsファイルは次のようになる。
class A[T] < Array[T] end
なおこのコマンドは生成された.rbsファイルの構文が正しいことを前提としている。
rbs prototype rbはしばしばsyntax errorのコードを生成する(これはバグなので直すべきだと思う)ので、このコマンドがうまく動かない場合、まずsyntax errorを直す必要があるだろう。
syntax errorを修正するコミットの例
- https://github.com/pocke/rbs_rails/pull/37/commits/c873094b8f3231bf618ea0143a22519e749369d1
- https://github.com/pocke/rbs_rails/pull/37/commits/69cc1eb1f3c5e7df9e5c1c39dfa3c7ecbe39bff3
ActiveSupport::ConcernによるClassMethodsモジュールをextendする処理を実行
ActiveSupport::Concernをextendしたモジュールをincludeすると、そのinclude先のモジュール/クラスにClassMethodsモジュールがextendされる。
module M extend ActiveSupport::Concern class ClassMethods def foo end end end class C # これは同時に extend M::ClassMethods も行う include M end
ところがこのメタプログラミングは RBS の世界では行われないため、別途手動でextendする必要がある。
そのために、該当のモジュールを自動でextendするスクリプトを作成した。
$ ruby bin/postprocess.rb -rlogger -rmutex_m -I assets/sig -I sig assets/
上記コードのように実行すると、各種ライブラリを読み込んだ上でassets/下のファイルについて必要があればextend ClassMethodsする。
rbs validateを実行して、エラーになったところを直す。
これが一番地味で、めんどくて、時間がかかるところ。
今までの作業で生成したファイルをrbs validateコマンドで検証して、怒られたところを直していく。
主に必要な作業は次の通り。
- 足りない型を足す
- 例えば
class Foo < Loggerと書かれていたら、Loggerクラスの型が必要になるので足す。 - rbs gem が型を提供しているライブラリであれば、
-rloggerのように require すれば良い。提供されていなければ、仮の型を書く。
- 例えば
- 継承元で定義されたクラスの参照を修正する
- メタプログラミングで定義されたクラスが必要になる場合、それを足す。
など、他にもいくらかやることがある。
ここまでやってrbs validateが通れば作業は完了で、Steep から生成した RBS ファイルを読み込んで使えるようになる。
ただし使えるようになると言っても多くの型はuntypedになっているため、より実用的にするには手動で型を埋めていく必要がある。
これはまだやっていない。
こんな感じで rbs_rails で提供する Rails の型を増やしていっている。 今の所、activesupport, activemodel, activerecord, railties の型を生成した。