はじめに
こんにちは、株式会社Techouseでバックエンドエンジニアをしている、新卒1年目のAsagaKoshoです。
2025年4月16日〜18日に愛媛県松山市にて開催されていたRubyKaigi 2025に参加してきました。
この記事では、@sinsokuさんによるDay1の発表 "Automatically generating types by running tests" (発表要旨、スライド)をご紹介します。
Rubyにおける型定義の記述について
Rubyは動的型付け言語であり、型宣言に該当する文法が存在しません。
一方で、Rubyで型を扱う手段が一切ないわけではなく、型宣言を記述するための言語であるRBSや、RBSを用いて型検査をするgemであるSteepなどが開発されています。
RBSでは、型宣言をRubyスクリプトとは分けて、.rbs
ファイルに記述する必要があり、コードと型情報の対応を追うのが大変です。そこで、Rubyスクリプト内にコメントとして型宣言を記述できる、RBS::Inlineも開発されています。
RBS::Traceとは
さて、ここからが本題となる型宣言の自動生成についてです。
RBS::Traceは、この発表のスピーカーである@sinsokuさんによって開発されているgemで、テストを実行することで型宣言を自動で記述してくれるツールです。
テストから型宣言を生成するとは
テストの実行で自動的に型宣言を生成するとはどういうことなのでしょうか。
RBS::Trace
では、テストケースで想定されている入出力の型に応じて、RBSの型コメントが生成されます。
テストケース
RSpec.describe "Calculator" do describe ".sum" do context "when args are Integer" subject { Calculator.sum(1, 2) } it { is_expected.to eq 3 } end end
RBSの型コメントが追加されたRubyスクリプト
class Calculator # @rbs (Integer, Integer) -> Integer def self.sum(x, y) x.to_i + y.to_i end end
スライド 14ページより抜粋
これを見て、既存のプロジェクトにRBSを導入する際、多量の型コメントを手で書くことなく自動生成できるという点に大きなメリットを感じました。
テストケースに不備がある場合は
RBS::Trace
では、テストケースをもとにして型コメントを生成するため、テストケースに不備がある場合には適切な型コメントが生成されません。
例えば、以下のような場合に問題となります。
- 想定している入出力が複数の型のUnionである場合に、テストケースでそのうちの一部のみしか記述していない場合
- テストケースが誤っている場合
- そもそもメソッドに対するテストケースが記述されていない場合
いずれも当たり前のことではありますが、RBS::Trace
をフル活用するためには、テストを網羅的に・不備なく記述しておく必要があります。
Railsアプリでも利用可能
RBS::Trace
はもちろんRuby on Railsアプリでも利用可能です。
この発表では、実際にRedmineやMastodonといったRuby on Rails製のOSSのコードに対して、型コメントの自動生成をした結果が紹介されました。 その結果、いくつかの問題はありましたが(後述)、型コメントを出力できました。 実際に追加された型コメントは、以下で見ることができます。
パフォーマンス面
RBS::Trace
はテストの実行時に同時に実行されるため、テストの実行時間に影響が出るのは避けられません。
Redmine、Mastodonでのテスト実行時間への影響は、以下のようだったとのことです。
スライド 23ページより抜粋
このように、テスト実行時間は4〜5倍程度にはなりますが、そもそもRBS::Trace
を実行して型コメントを付与する必要があるのは1度のみのため、大きな影響はありません。
どのように実装されているのか
RBS::Trace
は、Rubyの組み込みクラスである、TracePoint
を使って実装されています。
TracePoint
では、さまざまなイベントをトレースし、そのイベントに関する情報(例えばメソッドの呼び出しであれば、そのメソッドを定義するクラス名や、パラメータ定義など)を取得できます。
class User def initialize(first_name, last_name) p("initialize") end end TracePoint.trace(:call) do |tp| p(defined_class: tp.defined_class, method_id: tp.method_id, parameters: tp.parameters) end User.new("Yukihiko", "Matsumoto") # 実行結果 # {defined_class: User, method_id: :initialize, parameters: [[:req, :first_name, [:req, last_name]]} # "initialize"
スライド 30ページより抜粋
RBS::Trace
ではTracePoint
を活用することによって、メソッドの引数や返り値の型を取得し、これを型コメントに反映しています。
開発時に起きた課題
RBS::Trace
の開発時には、いくつかの問題が発生したそうです。(いずれも解消済み)それらの問題についてご紹介します。
class
メソッドを呼び出す際に、NoMethodError
が発生
Redmineでは、BasicObject
を継承したクラスがあり、BasicObject
はclass
メソッドを持たないため、TracePoint
でクラス名が取得できない問題が発生しました。この問題はUnboundMethod
を使うことにより解決したそうです。
ActiveRecordを継承したクラスの型が、ActiveRecord::Relation
になってしまう
Railsアプリケーションで、ActiveRecordを継承したクラスの型が、上書きされてしまい、ActiveRecord::Relation
になってしまう問題もありました。こちらも、UnboundMethod
を使うことにより解決したそうです。
void
型に非対応
当初、void
型に対応できていませんでした。上記でも説明したように、メソッドの引数や返り値の型を取得するという実装のため、返り値がない場合にvoid
の型コメントを追加できていませんでした。
この問題は、Prismの解析処理と組み合わせることで、解消したとのことです。
並列テストで動作しない
並列テストで期待通りに動作しない問題もありました。それは、同じメソッドに対して複数のテストがある場合、並列テストのそれぞれのプロセスで型コメントが書き込まれるため、すでに型コメントが書き込まれている箇所に対して再度型コメントを追加するという挙動が起こり得ます。その際には、新たなコメントでの上書きはできないため、出力される型コメントが不十分なものになるという問題がありました。
この問題に対しては、並列実行したテストを一度RBSファイルとして書き出したのちにマージして、その結果を型コメントとして追加することで解決されました。
スライド 60ページより抜粋
まとめ
RBS::Trace
は、既存の比較的大きなプロジェクトにRBSを導入する際、大きな力を発揮するツールだと感じました。私が関わっているプロジェクトでもRBSを導入することがあれば、フル活用できればと考えています。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。