RBS::Inline を導入してみました (1)


2024年 10月 17日

RubyKaigi 2024 の Embedding it into Ruby code というトークで RBS::Inline が発表されてから、4ヶ月が経ちました。

その間にいくつかの導入事例、利用記事を見かけてきましたが、タイミーさんの 前編:YARD から rbs-inline に移行しました という記事を読んで、重い腰を上げて RBS::Inline を導入することにしました。

この記事では、RBS::Inline を導入するにあたってハマったところや工夫が必要だった箇所について紹介します。

通常の導入や文法についての説明は省きますので、そのあたりは 公式の Syntax guideやタイミーさんの記事を見てください。

どの構文を選ぶのか

RBS::Inline ではコメント形式で型情報を記述します。

attr_* アクセサの型や定数の型、メソッドの型など、さまざまなものに型コメントをつけることができるのですが、
このうち、メソッドの型の記述には 3種類のコメント形式が提供されています。

※ RBS::Inline が公開された 5月の時点では、コメント形式はまだ決定していないというお話でした。先日の rbs-inlineを導入してYARDからRBSに移行する のトークで、構文が確定したという宣言がありました。

  • YARD 風コメント
    • パラメータや返り値ごとの型を記述する
    • 引数の説明をつけることもできる
# @rbs name: String -- 氏名
# @rbs age: Integer -- 年齢
# @rbs return: String -- あいさつを表す文字列
def hello(name, age)
  ...
end
  • メソッド全体の型コメント
    • メソッド全体の型定義を RBS の構文で記述する
#: (String name, Integer age) -> String
def hello(name, age)
  ...
end
  • 返り値の型コメント
    • def の後ろにコメントを付け、返り値の型を記述する
    • 引数の型は記述できないので、YARD 風コメントと併用する
def hello(name, age) #: String
  ...
end

3つの構文のうち、どれを使って型コメントを書いていくのか、一度チームメンバーと相談してみました。
その際には以下のような意見が出ました。

  • RBS 形式でかける、メソッド全体の型コメントがシンプルで良いのでは
  • YARD 風コメントは読みやすい
  • YARD 風コメントは引数の数だけコメント行が増えるのでコメントが長くなり、冗長である
  • 他の型コメントでは行末の #: で表現しているので、返り値の型コメントを採用するのは統一感がありそう
  • Steep 向けのインラインコメントでも後置 #: が使える (参考)
  • 引数を取らないメソッドの場合、返り値の型コメントはシンプルに書ける
  • 返り値の型コメントは、型に不慣れなメンバーでも書いてもらえるのでは
  • 引数が長い、もしくは型が複雑な場合は RBS 形式で書くと読みづらい

しばらく議論をしましたが、これといった決め手がなかったこともあり、先行して RBS::Inline を触っていた僕の好みをベースに、以下のルールで進めることになりました。

  • YARD 風コメントと返り値の型コメントを組み合わせる
  • 引数によって返り値が異なる場合(オーバーロードの場合)は、メソッド全体の型コメントを利用する

しばらくこのポリシーで書き進めてみて、もう少し経験を積んでから、どの形式が良いのか改めて検討してみたいと思います。

Rubocop とのコンフリクト

RBS::Inline の型コメントでは、 #: という形式のコメントをよく利用します。

attr_* アクセサや定数に型を付ける際に使えますし、メソッドの型を記述する際にも使えます。

MAX_COUNT = 100 #: Integer

attr_reader :name #: String

#: () -> void
def say_hello
  puts "hello"
end

しかし、この #: 形式のコメントは 3つの Rubocop のルール(cop)とバッティングすることがわかっています。

これらの指摘の回避方法は現状では存在しません。そのため、.rubocop.yml でこれらの cop を無効化することにしました。

# for RBS::Inline
Style/AccessorGrouping:
    Enabled: false

Style/CommentedKeyword:
    Enabled: false

Layout/LeadingCommentSpace:
    Enabled: false

しかし、このアプローチはあまり好ましくないですよね。僕らは型コメントを書きたいのであって、変なコメントは検出したいのです。この設定ではせっかくの cop が有効活用できません。

ということで、Rubocop に対して RBS::Inline の型コメントを受け入れるよう変更を提案してみました。

その結果、これらは本体にマージされることになりました。10/4 現在、まだリリースはされていないのですが、次のリリース以降では設定をひとつ追加するだけで RBS::Inline の型コメントが利用できます。

Layout/LeadingCommentSpace:
    AllowRBSInlineAnnotation: true

リリースが待ち遠しいですね。

10/17追記: この修正を含んだ 1.67.0 がリリースされていました🎉

module-self-types に複数のモジュールが書けない問題

手元には concern や helper など、module-self-types に複数のモジュールが指定されているモジュールがありました。(参考: module-self-types については RBS の self-type を理解する をお読みください。)

module ApplicationHelper  : ActionController::Base, ActionView::Base
  ...
end

しかし、これを RBS::Inline の # @rbs module-self という型コメントで記述しても、先頭のモジュールしか認識されません。これはバグっていますね…

# @rbs module-self ApplicationController::Base, ActionView::Base
module Kernel
  # => ApplicationController::Base だけが認識される
end

こういうときは、フットワーク軽くバグを直しましょう。

ということで、RBS::Inline にPR を送ってみたところ、すぐにマージしていただきました。RBS::Inline 0.8.0 ではこの問題が解消されており、複数の module-self-types が記述できるようになっています。

まとめ

ここまでくれば、あとはひたすら書き換えるだけですね。試しに導入した Rails アプリには 170ファイル強の型が存在していたのですが、もくもくと1日程度で書き換えが完了しました。実際には、小出しに PR を送り、型コメントの解説をしたり、ついでに既存の型を見直ししたり、寄り道をしながら反映をしていたので、すべての変更がマージされるまでには 3週間程度かかっています。

Rails そのものであるとか、ライブラリといった、アプリケーション以外のモジュールに対する型は .rbs ファイルに記述していますが、それ以外はすべて RBS::Inline に移行が完了しています。

この記事では RBS::Inline の導入にあたってハマったところを紹介しました。

  • どの構文を選ぶのか
  • Rubocop とのコンフリクト
  • module-self-types に複数のモジュールが書けない問題を手直しした

これから RBS::Inline を試してみようという方の参考になれば幸いです。