Ruby 3.3リリース! 新機能解説

新機能ラッシュ! RBS最新情報をキャッチアップ

RubyではRBSという言語で型を記述できます。Ruby 3.2から3.3では、RBSは2.8から3.4にバージョンアップしました。

RBSではこの1年でさまざまな機能追加やバグ修正が活発に行われ、520個のファイルに変更が加えられ、56,464行の追加と、26,172行の削除が行われました

私、栗原はその内の95個のファイルに変更を加え、7474行の追加と2341行の削除に関わりました。

この記事では、この一年RBS界隈を追い続けてきた私から、RBSのアップデートを中心にRubyの型の世界の変化をまとめて紹介します。Rubyで仕事をしている方、RBSに興味はあるけど最新情報は追えていなかった方の力になれれば幸いです。

RBSとは

Ruby 3.0からRubyに標準添付されている型システムの総称であり、ライブラリ名でもあります。RBSファイルは、Rubyファイルとは別のファイル・別の言語で記述することが特徴です。class/module名とメソッド名に引数の型と返り値の型を記述します。よくある説明としては、C言語でいう.c.h、TypeScriptでいう.jsd.tsの関係に似ていると言われています。

# foo.rbs
class Foo
  # Integer型を引数にとり、String型を返り値として返すメソッド
  def bar: (Integer) -> String
end
# foo.rb
class Foo
  # 実装は別ファイル
  def bar(i)
    i.to_s
  end
end

型と一口に言ってもさまざまな利用方法がありますが、RBSではこれまでに、Rubyの静的型チェックツールや型推論、コードの入力補助やドキュメント等、さまざまな使われ方が広まってきています。

rbsはGitHubリポジトリにて活発に開発されています。

ちなみに、本稿では大文字での⁠RBS⁠はファイルや文法やシステムを指し、小文字での⁠rbs⁠はコマンド名かGitHubリポジトリ自体を指しています。

RBSがメジャーバージョンアップ

RBSはこの1年でメジャーバージョンアップし、2系から3系に進化しました。といっても内部APIが少し変わったくらいで、これまでのRBSファイルはそのまま使え、新しい構文がいくつか追加されています。追加された構文を簡単に紹介します。

class/moduleの別名が定義可能に

RBSではclassやmoduleを定義できますが、このclass/module名の別名を定義できるようになりました。

class Queue = Thread::Queue

RubyではQueueと書くとThread::Queueclassとして使用できます。この表現をRBSでも書けるように、新しい構文が追加されました。Rubyとは違い、明示的にclassmoduleと最初につけることで別名を定義できます。

ファイル単位でのclass/module名の省略⁠別名が定義可能に

TypeScriptやPython等でお馴染みの、ファイル単位でのネームスペースの省略ができるようになりました。

use Happy::New::Year as Y

上記のclass/moduleの別名でも短い名前を定義できますが、定義された名前はグローバルに扱われてしまいます。use構文を使うことで、よりカジュアルに別名を扱えるようになりました。

“あとで直す”型の追加

構文というより型の追加ですが、__todo__という型が追加されました。

def foo: () -> __todo__

__todo__untypedの別名扱いですが、__todo__「まだ型を考えることができなくて仮の状態」であることを明示できるようになります。型としての判定はuntypedと同じですが、untypedはすべての型を許すという意味合いがある一方、__todo__では修正すべき仮の状態であることがわかりやすくなります。

新構文についてはMoney Forward Developers BlogのRBS に最近追加された構文に、より詳しい例を交えた紹介記事もあるのであわせてご覧ください。

RBSをテストする機能の追加

たとえば、minitestやrspecで記述されたテストコードに、RBSの正確性を担保するテストを追加できると、うれしいと思いませんか? これがついに可能になりました。

# Fooのインスタンスメソッド`#bar`を
# Integer型と共に呼び出すとString型が返ることを確認するテスト
assert_send_type "(Integer) -> String",
          Foo.new, :bar, 42

これまでrbsリポジトリー内だけで使える、RBSをテストするための機能があり、coreやstdlibのRBSはこの仕組みによって正確性が担保されています。この仕組みがパブリックAPIとして使用できるようになりました。

せっかくRBSを記述しても、それが正しくRubyコードの挙動を表現できているか保証はありません。Integerが返るメソッドにStringが返るように書いてしまっているかもしれません。

記述したRBSをテストできれば、RBSの記述が実装に沿った正しいものなのかをテストコードとして記述できるようになります。テストコードにできれば、CIとして確認できるようになり、RBSの新鮮さを保てるようになります。

RBSのテストはまだまだ開拓が進んでいない分野なので、このパブリックAPI化によってさらなる発展が期待されます。

Release noteにもminitestでの使用例が記載されています。

RBSのバリデーション機能が大幅アップデート

rbs validateコマンドで複数の改修があり、より使いやすく便利になりました。このコマンドは、ほとんどの状況でRBSの整合性チェックとして使える便利なコマンドなので、精力的な改修がなされました。

複数のエラーの報告に対応

これまで1つだけだった問題報告が、複数の問題を報告してくれるようになりました。エラー表示もコードの内容と指摘箇所を同時に表示されるようになり、非常にわかりやすくなりました。

$ rbs -I t.rbs validate
W, [2023-12-31T13:29:27.893150 #32732]  WARN -- rbs: t.rbs:2:11...2:25: `void` type is only allowed in return type or generics parameter (RBS::WillSyntaxError)

    def foo: (void) -> void
             ^^^^^^^^^^^^^^

E, [2023-12-31T13:29:27.904694 #32732] ERROR -- rbs: t.rbs:6:17...6:25: Could not find Integger (RBS::NoTypeFoundError)

    def bar: () -> Integger
                   ^^^^^^^^

逆に「これまでのように1つだけ問題を報告してほしい」というニーズも考えられるため、--fail-fastというオプションも追加されました。

より厳密なチェック

voidinstanceselfclassの使用がより厳密になり、想定しない使われ方をしている場合にwarningが出るようになりました。

例として以下のようなRBSが含まれている場合、指摘されるようになります。

class Sample
  # voidは返り値でのみ使える
  def f1: (void) -> untyped

  # instanceが使えるのはmethodだけ
  CONST: instance
end

この挙動は、はじめはerrorになるように実装されていたのですが、gem_rbs_collection内のRBS定義にこの変更によって新たにerrorになるようなRBSが見つかりました。また、gemに含まれているRBSまでは調べきれないこともあり、デフォルトではwarningにしたほうがいいのでは?と筆者が声を上げてwarningとなりました。

このwarningは将来的にRBSのSyntax Errorとなる予定なので、警告を見つけたら報告するか修正することを強くオススメします。

また--exit-error-on-syntax-errorというオプションをつけることで、warningがerrorになるように挙動を変更することもできます。もし状況的に問題ないのであれば、こちらのオプションをつけておくと将来的な移行がスムーズになるでしょう。

RBS同士を組み合わせる機能の追加

rbs subtractコマンドは、RBS同士を組み合わせて新たなRBSを生成するための新コマンドです。RBSの引き算(subtract)をすることによってRBS定義の重複を排除した出力ができるようになっています。

例として、手書きしたhandwritten.rbsは最優先にして、prototype等で自動生成したgenerated.rbsから同じメソッドは削除したい場合は以下のようにします。

$ rbs subtract --write generated.rbs handwritten.rbs

このコマンドをrbs prototypeと組み合わせてrake taskとして登録しておくことで、複数のRBS生成ツールを組み合わせることが容易にできるようになりました。

より詳しくはRubyKaigi 2023での作者の発表を観てください。

プロトタイプ生成機能もより充実

プロトタイプ生成機能もより充実しました。

インスタンス変数もプロトタイプ生成できるように

rbs prototype rbコマンドは、Rubyファイルを静的に読み込んでRBSのプロトタイプを生成するコマンドです。

これまでの出力に加えて、インスタンス変数・クラス変数・クラス内のインスタンス変数についてもプロトタイプの型を生成する機能が追加されました。

メタプログラミングによるプロトタイプ生成機能が大幅パワーアップ

rbs prototype runtimeコマンドは、rbs prototype rbコマンドに似ていますが、一度Rubyコードをrequireで読み込み、Rubyのメタプログラミングを利用してRBSのプロトタイプを生成する機能です。

便利なオプション機能の追加

  • class/module名の定義だけ欲しい場合のため--outlineオプションが追加されました。
  • RBSとしてまだ用意されていないメソッドのみ出力する--todoオプションが追加されました。プロトタイプなので、そのままRBS追加の足掛かりにできます。coreのメソッドでも調べてみると意外と実装されていないメソッドもあったりするので、⁠rbsに何か貢献したい」といった人のサポートにもなります。
  • 解析対象を拡張するため--autoloadオプションが追加されました。遅延読み込み設定にされているclass/moduleを、あえて読み込むことができます。このオプションは将来的にデフォルトで有効になる予定です。

StructとDataに丁寧に対応

これまでコミュニティーでは、⁠StructとDataを継承したclassはどのようなRBSを書けばいいのか?」という議論が度々上がっていました。そこでrbs prototype runtimeではStructとDataのメンバー名に合わせたオススメのRBSを特別に生成することで、参考となるRBS定義を示す機能が追加されました。

多くのバグ修正とパフォーマンスの向上

特定のケースでは100倍性能が向上するなど、パフォーマンスの向上や多くの不具合修正が行われました。このコマンドはまだまだ改善の余地が大きいので今後に期待です。

カジュアルにRBS構文が試せるように

rbs parseコマンドはRBSのシンタックスチェックをするコマンドです。アップデートによって、直接RBSを与えてシンタックスを確認できるようになりました。また、parseする種類も指定できるようになりました。

RBSの内容をファイルではなく直接書きたいときは-eオプションをつけることで実現できます。また、メソッドの型だけでいい場合は--method-typeオプションを、型単体だけでいい場合は--typeオプションをあわせてつけられるようになりました。

以下にProc型を試したときの例を示します。どうやらSyntaxを間違っていることがワンライナーで確認できました。間違っている箇所もわかりやすく表示されるようになっています。

$ rbs parse --type -e '^() >> Integer'
-e:1:4...1:6: Syntax error: expected a token `pARROW`, token=`>>` (tOPERATOR) (RBS::ParsingError)

  ^() >> Integer
      ^^

「結局何が変わったの?」がみつけられるように

rbs diffという新コマンドは、GitHub等でのプルリクエスト作成時にとくに役立ちます。RBSファイル間の大きな差分を効率的にレビューできるように作られたコマンドです。rbs diffではA環境とB環境に読み込まれたRBSのセットから、特定のクラスに関する差分を抽出し、markdownのtableや、diff形式で表示できます。これにより、何が変更され、何が追加または削除されたのかをレビュアーが容易に把握できるようになります。

使い方の例として、timeライブラリをrequireしたら、どのメソッドがTimeclassに追加されるのか一覧をmarkdown形式で出して、出力を掲載しておきます。

$ rbs diff --format markdown --type-name Time --after stdlib/time
before after
- def rfc2822: () -> ::String
- alias rfc822 rfc2822
- def httpdate: () -> ::String
- def xmlschema: (?::Integer fraction_digits) -> ::String
- alias iso8601 xmlschema
- def self.zone_offset: (::String zone, ?::Integer year) -> ::Integer
- def self.parse: (::String date, ?::Time::_TimeLike now) ?{ (::Integer) -> ::Integer } -> ::Time
- def self.strptime: (::String date, ::String format, ?::Time::_TimeLike now) ?{ (::Integer) -> ::Integer } -> ::Time
- def self.rfc2822: (::String date) -> ::Time
- alias self.rfc822 self.rfc2822
- def self.httpdate: (::String date) -> ::Time
- def self.xmlschema: (::String date) -> ::Time
- alias self.iso8601 self.xmlschema

ただ構文の差分を取るのではなく、overloadやclassの継承やmoduleのmixinを解決した後の差分を一覧で確認できるので、⁠結局何が変わったのか」を知ることができます。語幹が似ているrbs subtractとの違いを簡単に説明すると、rbs subtractが静的な重複記述の除去、rbs diffは動的な差分の表示を目的にしています。

数多くのRBS定義がアップデート

数え切れないほどのRBS定義が見直され、追加や修正が行われました。

個人的にとくに印象的だったRBS定義の変化は、Object classに定義された数多くのメソッドがまるごとKernel moduleへ移動されたことです。これまではRBSのメソッドに対するドキュメンテーションがrdocとリンクしていたため、RubyではKernelに定義されているメソッドがObjectに定義されていました。しかしながら、このドキュメンテーションの問題も解消されていたため、Rubyの実装に合わせてObjectからKernelへメソッドのRBS定義が移されました。

時を同じくして同じような議論がRuby側でもなされていたようで、rdocと実装でメソッドの定義位置が違うことが初学者に混乱を生んでいました。結論としては、実装と同じようにドキュメントも移動するようになったようです。

周辺ライブラリの変化

最後に、周辺ライブラリの変化を取り上げます。

Steep

RBSの生みの親である、Rubyコードの静的型チェッカーです。上記RBSのアップデートに対応しつつ、LSPの機能を活用したアップデートが複数なされています。より使いやすく実践的なツールに進化しています。

TypeProf

RBSを書かなくても型を推論するという野心的なツールですが、RubyKaigi 2023でTypeProfのリブートが発表されました。LSPを主要なユースケースにすえ、大幅なパフォーマンス向上を目指して開発されています。ただし、このリブート版はRuby 3.3にはバンドルされていません。

IRB

Rubyの標準的なREPLで、最近RBSを利用した補完機能が大きな注目を集めました。型があることで補完が速くなるというおもしろいアプローチをとっています。

Sorbet

StripeとShopifyが力を入れて開発している、もう1つのRubyの型プロジェクトです。

残念ながら筆者がSorbetについてキャッチアップできていないのですが、高速なチェッカー、充実の型ファイル自動生成、カバレッジのグラフ表示と、エコシステムとしてのSorbetはRBSよりもずいぶん先を行っていると考えており、参考実装としてリスペクトしつつ後を追いかけていきたいものです。

Orthoses

手前味噌ですが、RubyKaigi 2022とKaigi on Rails 2023で発表されたRBSの自動生成システムです。基本的な機能は実装済みですが、さらに発展的な機能を盛り込もうとrbs本体に手を伸ばして絶賛開発中です。

今後のRBS

今後は、新たなシンタックスの追加、パフォーマンスの向上、著名ライブラリでのRBS同梱、テストでの活用など、さまざまな方向への発展が期待されています。

Rubyの型は、まだまだ未開拓な部分が多く、これからの発展が非常に楽しみです。

おすすめ記事

記事・ニュース一覧