pockestrap

Programmer's memo

rbs_railsにRailsの型を取り込む手順 2020-07-16版

なんか色々がちゃがちゃやっていて面白いので、メモしておく。

rbs_rails とは

github.com

RailsRBS (Rubyの型定義ファイル)を扱うためのライブラリ。

rbs_railsRails の型を取り込む」とは

rbs_rails には2つの機能がある。

1つは、ユーザーの書いたRailsアプリケーションのコードから、型定義ファイルを生成すること。 例えば、usersテーブルにaccountというString型のカラムがあったら、Userクラスの型にaccountというStringを返すインスタンスメソッドを定義する。 こちらの機能は今回の記事には関係ない。

もう1つの機能は、Rails 自体の型定義を提供すること。 例えば Rails のコードを読み込むとActiveRecord::Baseクラスが定義されるが、それを型として使うにはActiveRecord::BaseRBS でも定義する必要がある。 そのため、rbs_railsRails 自体の型定義を提供している。

この Rails 自体の型定義を提供するにあたって、これを手書きしていたら時間がいくらあっても足りない。 そのため、Railsソースコードから型定義を自動生成した上で、それを手直しして提供している。

この記事ではその Rails 自体の型定義をどう自動生成しているかについて書く。

生成手順

以下の手順で生成している。

  1. rbs prototypeコマンドで RBS ファイルを生成
  2. 型パラメータを埋めるコマンドを実行
    • 1で生成した RBS ファイルに Syntax Error などがあるとこの処理がコケるので、適宜手で直す
  3. ActiveSupport::ConcernによるClassMethodsモジュールをextendする処理を実行
  4. rbs validateを実行して、エラーになったところを直す。

以下に各 step を解説する。

rbs prototypeコマンドで RBS ファイルを生成

rbsコマンドが提供するrbs prototypeサブコマンドを使うと、RBS ファイルのプロトタイプが生成できる。

Ruby プログラムから RBS を生成するにはrbs prototype runtimerbs 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 rbclass_methodsを解釈しないため、このケースではfooは単なるMインスタンスメソッドとして定義されてしまう。 これを防ぐため、rbs prototype rbを拡張して使用している。

https://github.com/pocke/rbs_rails/blob/07d4a56b96d9e0d2d8a6006d7e1c0bf91d955818/bin/rbs-prototype-rb.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を修正するコミットの例

ActiveSupport::ConcernによるClassMethodsモジュールをextendする処理を実行

ActiveSupport::Concernextendしたモジュールを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 すれば良い。提供されていなければ、仮の型を書く。
  • 継承元で定義されたクラスの参照を修正する
    • Ruby は継承元のクラス内の定数を参照できるが、RBS はそれができない(mixin も同様)。
    • これは仕様である
    • ```ruby class A class B end end

      class C < A # Ruby ではこの B は A::B になるが、 # RBS ではその探索が行われないのでエラーになる class D < B end end ```

    • つまり、class D < A::Bのように明示的に書き換える必要がある。
  • メタプログラミングで定義されたクラスが必要になる場合、それを足す。

など、他にもいくらかやることがある。

ここまでやってrbs validateが通れば作業は完了で、Steep から生成した RBS ファイルを読み込んで使えるようになる。

ただし使えるようになると言っても多くの型はuntypedになっているため、より実用的にするには手動で型を埋めていく必要がある。 これはまだやっていない。


こんな感じで rbs_rails で提供する Rails の型を増やしていっている。 今の所、activesupport, activemodel, activerecord, railties の型を生成した。