pockestrap

Programmer's memo

RBS基礎文法最速マスター

RBSRuby 3に組み込まれた、Rubyの型情報を記述する言語です。

この記事ではRBSの文法を駆け足で紹介します。 細かい話は飛ばしますが、この記事を読めば大体のケースでRBSを読み書きできるようになると思います。

事前準備

インストール

まずは文法の前に、rbs gemをインストールしましょう。

Ruby 3を使っている場合、rbs gemはRuby 3に同梱されているため何もしなくても使えます。 Ruby 3未満を使っている場合でも、gem install rbsすれば使うことができます。

この記事では、rbs gem v1.0.0を対象に構文を紹介します。

$ gem install rbs
Successfully installed rbs-1.0.0
1 gem installed

$ rbs --version
rbs 1.0.0

動作確認

書いたRBSは、rbsコマンドを使うと簡単なチェックができます。

まず、rbs parseコマンドで構文チェックができます。

# valid.rbs

class C
end
# invalid.rbs

class C
ennnd
# 構文に問題がなければ何も出力せず exit 0 する
$ rbs parse valid.rbs
$ echo $?
0

# 構文に問題があればエラーを出力して exit 1 する
$ rbs parse invalid.rbs
invalid.rbs:4:0: parse error on value: (tLIDENT)
$ echo $?
1

またrbs validateコマンドを使うと、RBSの定義に問題がないか簡単なチェックができます。 スーパークラスの存在チェック、includeしているモジュールの存在チェック、aliasする先のメソッドの存在チェックなどを行えます。

# missing-superclass.rbs

class C < X
end

rbs validateコマンドの実行にはファイル名を指定するのではなく、rbsファイルを置いているディレクトリを-Iオプションで指定します。 -Iオプションを使用すると、そのディレクトリ内に存在する.rbsファイルを全て読み込んだ上でコマンドを実行します。

$ rbs -I . validate
/path/to/rbs-1.0.0/lib/rbs/errors.rb:114:in `check!': test.rbs:1:0...2:3: Could not find super class: X (RBS::NoSuperclassFoundError)
        from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:211:in `one_instance_ancestors'
        from /path/to/rbs-1.0.0/lib/rbs/definition_builder/ancestor_builder.rb:390:in `instance_ancestors'
        from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:141:in `block in build_instance'
        from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:765:in `try_cache'
        from /path/to/rbs-1.0.0/lib/rbs/definition_builder.rb:135:in `build_instance'
        from /path/to/rbs-1.0.0/lib/rbs/cli.rb:423:in `block in run_validate'
        from /path/to/rbs-1.0.0/lib/rbs/cli.rb:421:in `each_key'
        from /path/to/rbs-1.0.0/lib/rbs/cli.rb:421:in `run_validate'
        from /path/to/rbs-1.0.0/lib/rbs/cli.rb:113:in `run'
        from /path/to/rbs-1.0.0/exe/rbs:7:in `<top (required)>'
        from /path/to/bin/rbs:23:in `load'
        from /path/to/bin/rbs:23:in `<main>'

基礎構文

では、RBS言語の構文を確認していきましょう。

コメント

RBSではRubyと同様に#の後ろがコメントになります。

# RBS

# これはコメント

クラス・モジュール定義

RBSではRubyとほぼ同じようにクラス・モジュールを定義できます。

# RBS

# クラスの定義
class C
end

# モジュールの定義
module M
end

# ネストしたクラスの定義(1)
# (ただし、Cが定義されている必要がある)
class C::C2
end

# ネストしたクラスの定義(2)
class C
  class C2
  end
end

メソッド定義

RBSではRubyと同様にdefキーワードを使ってメソッドを定義できます。 ですが構文はRubyと多少異なり、def メソッド名: (引数の型) -> 戻り値の型という形です。

基本的なメソッド定義

基本的なメソッド定義は次のようになります。

# RBS

class C
  # 引数を受け取らず、nilを返すメソッド
  # Rubyだと def foo1() end
  def foo1: () -> nil

  # Integerの引数を1つ受け取るメソッド
  # Rubyだと def foo2(n) end
  def foo2: (Integer) -> nil

  # 同様の意味で、引数に名前をつけたもの
  # Rubyだと def foo3(arg_name) end
  def foo3: (Integer arg_name) -> nil

  # 省略可能なIntegerの引数を1つ受け取るメソッド
  # Rubyだと def foo4(n = 42) end
  def foo4: (?Integer) -> nil

  # 任意個のIntegerの引数を受け取るメソッド
  # Rubyだと def foo5(*n) end
  def foo5: (*Integer) -> nil

  # 必須のIntegerのキーワード引数を受け取るメソッド
  # Rubyだと def foo6(n:) end
  def foo6: (n: Integer) -> nil

  # 省略可能なIntegerのキーワード引数を受け取るメソッド
  # Rubyだと def foo6(n: 42) end
  def foo6: (?n: Integer) -> nil

  # 値がIntegerの任意のキーワード引数を受け取るメソッド
  # Rubyだと def foo7(**kwargs) end
  def foo7: (**Integer) -> nil

  # 引数を受け取らずnilを返す必須のブロックを受け取るメソッド
  # Rubyだと def foo8(&block) block.call() end
  def foo8: () { () -> nil } -> nil

  # 引数を受け取らずnilを返す省略可能なブロックを受け取るメソッド
  # Rubyだと def foo9(&block) block.call() if block_given? end
  def foo9: () ?{ () -> nil } -> nil

  # Integerの引数を1つ受け取るブロックを受け取るメソッド
  # Rubyだと def foo10(&block) block.call(42) end
  def foo10: () { (Integer) -> nil } -> nil
end

特異メソッドの定義

特異メソッドを定義する場合、メソッド名の前にself.を追加します。それ以外はインスタンスメソッドの定義と同じです。

# RBS

class C
  # C.foo の定義。
  # Rubyだと def self.foo() end
  def self.foo: () -> nil
end

メソッドのエイリアスの定義

インスタンスメソッド、特異メソッドへのaliasも定義できます。

# RBS

class C
  def foo: () -> nil
  def self.foo2: () -> nil
  
  # fooメソッドと同じ型を持つbarメソッドを定義する
  alias bar foo
  
  # 特異メソッドのaliasも定義できる
  alias self.bar self.foo
end

attr_*の定義

attr_readerなどのattr_*を定義する構文も用意されています。

# RBS

class C
  # Integer型の値を返すfooメソッドの定義と、
  # Integer型の @foo インスタンス変数の定義
  attr_reader foo: Integer
  
  # attr_writer, attr_accessor も同様
  attr_writer bar: Integer
  attr_accessor baz: Integer
end

また特異クラスのattr_*も定義できます。

# RBS
class C
  # 特異クラスの attr_reader の定義
  # Rubyだと以下
  #   class << self
  #     attr_reader :foo
  #   end
  attr_reader self.foo: Integer
end

メソッドのオーバーロード

|を使うとメソッドのオーバーロードを定義できます。

# RBS

class C
  # Integerを受け取ってStringを返すか、
  # Stringを受け取ってIntegerを返すメソッド
  def foo: (Integer) -> String
         | (String) -> Integer

  # 3つ以上のオーバーロードも書ける
  def foo: (Integer) -> String
         | (Float) -> Rational
         | (String) -> Numeric
end

|は慣習的にメソッド名の後ろのコロンの位置に揃えます。

エラーになるもの

Rubyと違い、クラス・モジュール定義の外ではメソッドを定義できません。

# RBS (構文エラー)

def foo: () -> Integer

また、class << self構文を用いた特異メソッドの定義はできません。

# RBS (構文エラー)

class C
  class << self
    def foo: () -> Integer
  end
end

1つのクラス・モジュールに同名のメソッドを2回以上定義するとrbs validateでエラーになります。

# RBS (RBS::DuplicatedMethodDefinitionError)

class C
  def foo: () -> Integer

  def foo: () -> String
end

インターフェイス

RBSではRubyにない要素として、インターフェイスの定義ができます。 インターフェイスinterfaceキーワードで定義し、インターフェイス名は必ずアンダースコアで始まります。

# RBS

# 引数を受け取らずnilを返すfooメソッドを持つインターフェイスの定義
interface _Fooable
  def foo: () -> nil
end

class C
  # _Fooable を満たす値を1つ受け取るメソッド
  def bar: (_Fooable) -> nil
end

インターフェイスはクラス/モジュールの中にも定義できます。 名前の衝突を避けるため、ライブラリが提供するインターフェイスはそのライブラリの名前空間の中に定義すると良いでしょう。

# RBS

module M
  interface _I
  end
end

class C
  # M::_I として定義したインターフェイスを参照できる。
  def foo: () -> M::_I
end

なお、インターフェイスの中にインターフェイスやクラス・モジュールを定義はできません。

# RBS (構文エラー)

interface _I
  class C
  end
end

モジュールのinclude, extend

モジュールのinclude, extendも同様に行なえます。

# RBS

module M
end

class C
  include M
  extend M
end

またインターフェイスも同様にinclude, extendできます。

# RBS

interface _Fooable
  def foo: () -> Integer
end

class C
  include _Fooable
  extend _Fooable
end

型引数

RBSではクラス/モジュール/インターフェイスに、また個別のメソッド定義に型引数を使えます。

クラス・モジュール・インターフェイスに対して型引数を使う例

例えば簡単なArrayクラスの定義と使用例は次のようになります。

# RBS

# Arrayの要素の型をElemとして定義したArrayクラス
class Array[Elem]
  def first: () -> Elem
end

class C
  # String型を要素に持つArrayクラスのインスタンスを返すメソッド
  def foo: () -> Array[String]
end

複数の型引数も定義できます。次は簡単なHashクラスの定義の例です。

# RBS

class Hash[Key, Value]
  def keys: () -> Array[Key]
  def values: () -> Array[Value]
end

class C
  # キーがSymbol、値がIntegerのHashを返すメソッドの定義
  def foo: () -> Hash[Symbol, Integer]
end

また、簡単なEnumerableモジュールの定義と使用例は次のようになります。

# RBS

# _Eachable は別途定義する必要がある
module Enumerable[Elem] : _Eachable
  def first: () -> Elem
end

class C
  # Stringに対してEnumerableが使えるクラス。
  # C#first は String を返す。
  include Enumerable[String]
end

メソッドに対して型引数を使う例

メソッドに対して型引数を使う場合、メソッドの型の前に[]を書きます。

# RBS

class C
  # 任意の型の値を受け取って、受け取った型と同じ型の値を返すメソッド。
  # Rubyでは例えば def foo(x) x; end など
  def foo1: [T] (T) -> T
  
  # 任意の型の値を2つ受け取って、それぞれを含むタプル型を返すメソッド
  def foo2: [T, U] (T, U) -> [T, U]

  # 型引数を受け取る場合にもオーバーロードが定義できます。
  def foo3: () -> nil
          | [T] (T) -> T
          | [T, U] (T, U) -> [T, U]
end

Type Alias

長い型のエイリアスtype構文を使って定義できます。エイリアス名は小文字始まりで定義します。 なおalias構文を使って定義するメソッド名のエイリアスとは関係のない別の機能です。

# RBS

# String か Symbol のどちらかを表す別名として name を定義する例
type name = String | Symbol

また、Type Aliasはクラス・モジュールの中に定義することもできます。 ライブラリが提供するType Aliasは名前の衝突を避けるため、クラス・モジュールの中に定義すると良いでしょう。

# RBS

module M
  type name = String | Symbol
end

class C
  # モジュールMで定義したエイリアスnameを参照する例。
  # このfooメソッドはStringかSymbolを返す。
  def foo: () -> M::name
end

定数

RBSでは定数の型も定義できます。

# RBS

# トップレベルの定数Xの型をStringとして定義する
X: String

class C
  # 定数C::Xの型をStringとして定義する
  X: String
end

インスタンス変数

RBSではインスタンス変数の型も定義できます。

# RBS

class C
  # @foo インスタンス変数の型をStringとして定義する
  @foo: String
end

ここまでで何の説明もなくnilIntegerを受け取る/返すと表現してきました。 nilIntegerを書いていたところには実際には色々な型を書くことが出来ます。 ここではそこに書くことができるものを紹介します。

Class Instance

一番良く使うのはクラスのインスタンスでしょう。単にクラス名を書くと、そのクラスのインスタンスを指す型となります。

# RBS

class C
  # Cクラスのインスタンスを返すメソッド
  # Rubyだと def foo1() C.new() end
  def foo1: () -> C
  
  # Stringクラスのインスタンスを返すメソッド
  # Rubyだと def foo2() "" end
  def foo2: () -> String
  
  # :: も使える
  def foo3: () -> Foo::Bar
end

Singleton

また、クラス自体を返したい場合にはsingletonキーワードを使います。

# RBS

class C
  # Stringクラス自体を返すメソッド。
  # Rubyだと def foo() String; end
  def foo: () -> singleton(String)
end

リテラル

一部のリテラルRBSでもリテラルとして表現できます。

# RBS

class C
  # 文字列 "x" を返すメソッド。
  # Rubyだと def foo1() "x" end
  def foo1: () -> "x"
  
  # シンボル :x を返すメソッド
  # Rubyだと def foo2() :x end
  def foo2: () -> :x
  
  # 数値 42 を返すメソッド
  # Rubyだと def foo3() 42 end
  def foo3: () -> 42
end

タプル

固定長の配列はタプル型として表現できます。

# RBS

class C
  # Integer 1要素だけを持つ配列を返すメソッド
  # Rubyだと def foo1() [42] end
  def foo1: () -> [ Integer ]

  # Integer 2要素を持つ配列を返すメソッド
  # Rubyだと def foo2() [42, 43] end
  def foo2: () -> [ Integer, Integer ]

  # 1つ目の要素にInteger,2つ目の要素にStringを持つ配列を返すメソッド
  # Rubyだと def foo3() [42, 'foo'] end
  def foo3: () -> [ Integer, String ]

  # [42]を返すメソッド。
  # Rubyだと def foo4() [42] end
  def foo4: () -> [ 42 ]

  # 空の配列を返すメソッド。
  # [と]の間にスペースが必要なのに注意
  # Rubyだと def foo5() [] end
  def foo5: () -> [ ]
end

レコード

固定のキーを持つHashはレコード型として表現できます。

# RBS

class C
  # xがキーでIntegerが値のHashを返すメソッド。
  # Rubyだと def foo1() { x: 42 } end
  def foo1: () -> { x: Integer }

  # ネストもできる
  def foo2: () -> { x: { y: { z: Integer} } }
end

なお空のレコード型は書けません。

# RBS (構文エラー)

class C
  def foo: () -> { }
end

Proc

Procクラスのインスタンスを表す構文も用意されています。^の後ろにメソッド定義と同様のコードを書くと、Proc型となります。

# RBS

class C
  # Integerを受け取ってStringを返すProcを返すメソッド
  # Rubyだと def foo1() proc { |int| int.to_s } end
  def foo1: () -> ^(Integer) -> String
end

組み込み型

RBSではいくつかの型が標準で用意されています。 ここではそのうち代表的なものを紹介します。

untyped

untypedは「型チェックがされないこと」を示す型です。TypeScriptのanyです。 とりあえず型検査を通す上ではuntypedを使うのが便利であるため、既存のRubyコードに型をつけていく場合にはuntypedの出番は多いでしょうl

self

レシーバと同じ型を示します。クラスを継承した場合、selfは継承先のクラスのインスタンスの型になります。

# RBS

class C
  def foo1: () -> self
  def foo2: () -> C
end

class D < C
end

例えばこの例の場合、C#foo1C#foo2はどちらもCクラスのインスタンスを返すメソッドです。 一方D#foo1Dクラスのインスタンスを返すメソッドになります。D#foo2C#foo2と変わらず、Cクラスのインスタンスを返します。

nil, true, false

それぞれNilClass, TrueClass, FalseClassインスタンスを表します。

def foo: () -> nildef foo: () -> NilClassは等価です。 ただし前者がより推奨されています。つまりNilClassよりもnilのほうが推奨されています。

boolとboolish

RBSは真偽値を示す型を2つ用意しています。boolboolishです。

booltrue | falseエイリアスです。

一方boolishは全ての型のsupertypeであり、真偽値としてのみ使える型を表します。 全ての型の値をboolish型として宣言された変数に代入できますが、boolish型は真偽値以外の用途(メソッド呼び出しなど)に使うことは出来ません。

# RBS

class C
  # trueかfalseのみを受け取るメソッドの定義
  def foo1: (bool) -> nil
  
  # どのような値でも受け取るメソッドの定義。
  # ただし受け取った値は真偽値としてのみ使える
  def foo2: (boolish) -> nil
end

void

使われない値であることを意味するときに使う型です。

class C
  # 戻り値が使われないメソッドの定義
  def foo: () -> void
end

union

複数の型のどれか1つを表す型です。|で表します。

# RBS

class C
  # StringかIntegerを受け取ってStringを返すメソッドの定義
  def foo: (String | Integer) -> String
end

なお、戻り値の型にunion型を使う場合にはカッコでくくる必要があります。 これはオーバーロードとの区別をするためです。

# RBS

class C
  # StringかIntegerを返すメソッドの定義
  def foo: () -> (String | Integer)
  
  # 以下は構文エラー
  def foo: () -> String | Integer
end

intersection

複数の型の全てを兼ね備えた型です。&で表します。

interface _Fooable
  def foo: () -> nil
end

interface _Barable
  def bar: () -> nil
end

class C
  # fooメソッドとbarメソッドの両方を持った型を受け取るメソッドの定義
  def x: (_Foo & _Bar) -> nil
end

optional

nilかもしれない値を表す型です。型の後ろに?を後置して表します。

class C
  # Integerかnilを返すメソッドの定義
  def foo: () -> Integer?
end

参考リンク

この記事では駆け足でRBSの構文を紹介しました。

より詳しくRBSについて知りたい方は、次の資料が参考になるでしょう。

また実際のRBSを参考にしたい場合、ruby/rbscore/, stdlib/, sig/ディレクトリ下を見ると良いでしょう。 core/ディレクトリ下にはビルトインのライブラリのRBSが、stdlib/ディレクトリ下には標準添付ライブラリのRBSが、sig/ディレクトリ下にはRBS自体のRubyコードに対するRBSが含まれています。


アドベントカレンダーには乗り遅れましたが、https://qiita.com/advent-calendar/2020/reiwa_saisoku を見て書いてみました。