こんにちは、tk0miya です。
前回の準備編では、rbs.rake を導入して Ruby の型定義を自動生成し、Rake タスクの初回実行後のエラーを解決するところまでを紹介しました。
今回は実際に型定義を書いていく部分に進んでいきます。
型定義を書いていく前に、ちょっと手を止めて型とはなにかをおさらいしましょう。
まずは以下の Ruby プログラムを見てみましょう。
def sum(x, y)
x + y
end
a = 1
b = 2
c = sum(a, b)
このプログラムを見て、あなたはどのように理解しましたか?
たとえば、a や b はどういうデータだと思いましたか?
sum 関数は何を返し、c にはどういう値が入るでしょうか。
Ruby プログラマの人は「a や b は Integer のデータが入っている」「sum は2つの数値の合計を返す」。そのため、「c にも Integer のデータが入る」と答えるのではないでしょうか。
これが型です。
型というのはあるデータがどういう種類のデータなのか、関数がどういう入出力なのかを表すものです。
別の言葉で言い換えると、
を表現したものです。
そして、型定義はこの型情報を記述言語で表現したものです。
Ruby の型定義ファイルは RBS と呼ばれる言語で記述します。
RBS では、先ほどのプログラムの型は以下のように表現します。
def sum: (Integer x, Integer y) -> Integer
a: Integer
b: Integer
c: Integer
※ 通常、ローカル変数には型定義を書かないのですが、ここでは説明のために敢えて書きました。
RBS の詳しい文法はここでは紹介を割愛します。 RBS基礎文法最速マスター – pockestrap がシンプルにまとめられたよい記事ですので、こちらを参照してください。
まえがきが長くなってしまいましたが、実際に型を書いていきましょう。
型を書くステップは以下のとおりです。
rbs:subtract
コマンドを呼び出して、型定義情報をアップデートするrbs:validate
コマンドを呼び出して、型定義をチェックする実は、これまでの準備段階で基本的な型定義はアプリケーションから自動抽出されています。
rbs collection
コマンド
rbs_rails
gem
rbs prototype rb
コマンド
ですので、型を書いていく際には、ツールによって自動抽出された型定義に対して、不足情報を補っていくというアプローチを取ります。
先ほど挙げたステップは、 rbs prototype rb
コマンドが生成した型定義をベースに加筆修正していくというものです。
さて、型をつけていこうというときに、どこから手を付けていいのか迷ってしまいますよね。
そういうときは、アプリケーションの中でも中心となるクラスに型をつけると効果的だと思います。多くの場所で使われるモデルやサービスクラス、Concern などに型をつけていくとよいでしょう。
なお、以前に紹介した katakata_irb はメソッドチェーンでの入力支援が魅力のひとつですよね。これを強化するために、メソッドチェーンの起点になるようなクラス、メソッドに対して型をつけるのは作戦のひとつです。
※ 今回の記事では紹介しませんが、型チェッカである Steep を LSP として利用する場合は、引数の型情報を入力支援に利用するため、余裕があれば引数の型も書いていけるとなおよいですね。
rbs prototype rb
コマンドで生成されたプロトタイプの型定義ファイルを sig/handwritten ディレクトリ以下にコピーします。
たとえば、 app/models/account.rb
に対する型定義は sig/prototype/app/models/account.rbs
に生成されているので、 sig/handwritten/app/models/account.rbs
にコピーします。
$ mkdir -p sig/handwritten/app/models
$ cp sig/prototype/app/models/account.rbs sig/handwritten/app/models
プロトタイプの型定義ファイルを直接書き換えるようにしている方々もいるようですが、わたしたちは必要な部分にだけ型をつける (なるべく自動抽出に委ねる) というアプローチで型を導入しようとしているため、プロトタイプの型定義と手書きの型定義は分けて管理しています。
このアプローチでは、メソッドが増えた場合でも、半自動的にメソッドの型定義が抽出されるため、型を書くのを手抜きできます。
ちなみに、毎回このディレクトリを掘ったり、ファイルをコピーしたりというのがかなり手間であったため、VSCode 拡張として RBS Helper というものを用意しました。
VSCode のコマンドひとつで、現在開いている .rb ファイルに対する型定義ファイルを生成する (プロトタイプの型定義があればそれをコピーする) という優れものです (自画自賛)。
先ほどコピーした型定義ファイルを書き換えます。
rbs prototype rb
コマンドが生成した型定義ファイルは、以下のようにメソッドが羅列されています。
class Account
def admin?: () -> untyped
def enjoy: (untyped sport) -> untyped
def favorites: () -> untyped
end
rbs prototype rb
コマンドはメソッドや引数の羅列には対応していますが、型は推測できないため、引数の型や返り値の型には untyped (型不明) がセットされています。
ここに対して、ソースコードを読みながら、実際の型に入れ替えていきましょう。
サンプルとして、それぞれのメソッドに型を書いてみました。
class Account
def admin?: () -> bool
def enjoy: (Sport sport) -> void
def favorites: () -> Array[SportTeam]
end
この例では
#admin?
メソッドは判定メソッドですので boolean を返し#enjoy
メソッドは Sport オブジェクトを引数に取り、値は返さず(void)#favorites
メソッドは SportTeam オブジェクトの配列を返すという型に書き換えています。
※ 先ほども紹介しましたが、RBS については RBS基礎文法最速マスター – pockestrap がおすすめです。
ここでは登場するすべてのメソッドの型を書き換えましたが、ソースコードの規模やご自身の余裕に応じて、必要な部分だけ書き換えるという選択をするのも良いでしょう。
untyped という型のままでもうまく動かないというわけではありません。
肩の力を抜いて、徐々に型を書いていくという考え方で良いと思います。
rbs:subtract
タスクを呼び出して、型定義情報をアップデートするさて、型定義を書き足したところで rbs:subtract
タスクを呼び出して、型定義情報をアップデートしましょう。
さきほど、ファイルをコピーしたため、同じメソッドに対する型が 2種類存在する状態になっています。
前回セットアップした Rake タスク rbs:subtract
は、手書きした型を優先して sig/prototype
ディレクトリ以下の型定義を間引いてくれます。
$ bundle exec rails rbs:subtract
rbs:validate
コマンドを呼び出して、型定義をチェックする最後に rbs:validate
コマンドを呼び出して、型定義の整合性をチェックしましょう。
不整合などがあればここでエラーが表示されます。
$ bundle exec rails rbs:validate
今回は Ruby の型定義のメンテの仕方についてご紹介しました。
自動抽出をベースとして、必要な部分だけ型を補うというアプローチに基づいた手順です。
このアプローチには、型に不慣れなメンバーと一緒に徐々に型に慣れていくという狙いがあります。
型の自動抽出をしながら、katakata_irb や LSP などを使って型の恩恵に預かっていくと、徐々に型の便利さを発見し、少しずつ前進して行けるのでは、という期待を込めています。
一連の記事が皆さんの参考になれば幸いです。