# RBS基礎文法最速マスター(下書き) https://pocke.hatenablog.com/entry/2021/01/02/175940 RBSはRuby 3に組み込まれた、Rubyの型情報を記述する言語です。 この記事ではRBSの文法を駆け足で紹介します。 細かい話は飛ばしますが、この記事を読めば大体のケースでRBSを読み書きできるようになると思います。 # 事前準備 ## インストール まずは文法の前に、rbs gemをインストールしましょう。 Ruby 3を使っている場合、rbs gemはRuby 3に同梱されているため何もしなくても使えます。 Ruby 3未満を使っている場合でも、`gem install rbs`すれば使うことができます。 この記事では、rbs gem v1.0.0を対象に構文を紹介します。 ```console $ gem install rbs Successfully installed rbs-1.0.0 1 gem installed $ rbs --version rbs 1.0.0 ``` ## 動作確認 書いたRBSは、`rbs`コマンドを使うと簡単なチェックができます。 まず、`rbs parse`コマンドで構文チェックができます。 ```ruby # valid.rbs class C end ``` ```ruby # invalid.rbs class C ennnd ``` ```bash # 構文に問題がなければ何も出力せず 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`する先のメソッドの存在チェックなどを行えます。 ```ruby # missing-superclass.rbs class C < X end ``` `rbs validate`コマンドの実行にはファイル名を指定するのではなく、rbsファイルを置いているディレクトリを`-I`オプションで指定します。 `-I`オプションを使用すると、そのディレクトリ内に存在する`.rbs`ファイルを全て読み込んだ上でコマンドを実行します。 ```bash $ 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と同様に`#`の後ろがコメントになります。 ```ruby # RBS # これはコメント ``` ## クラス・モジュール定義 RBSではRubyとほぼ同じようにクラス・モジュールを定義できます。 ```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 メソッド名: (引数の型) -> 戻り値の型`という形です。 ### 基本的なメソッド定義 基本的なメソッド定義は次のようになります。 ```ruby # 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 # 引数を受け取らずnilを返す必須のブロックを受け取るメソッド # Rubyだと def foo7(&block) block.call() end def foo7: () { () -> nil } -> nil # 引数を受け取らずnilを返す省略可能なブロックを受け取るメソッド # Rubyだと def foo8(&block) block.call() if block_given? end def foo8: () ?{ () -> nil } -> nil # Integerの引数を1つ受け取るブロックを受け取るメソッド # Rubyだと def foo9(&block) block.call(42) end def foo9: () { (Integer) -> nil } -> nil end ``` ### 特異メソッドの定義 特異メソッドを定義する場合、メソッド名の前に`self.`を追加します。それ以外はインスタンスメソッドの定義と同じです。 ```ruby # RBS class C # C.foo の定義。 # Rubyだと def self.foo() end def self.foo: () -> nil end ``` ### メソッドのエイリアスの定義 インスタンスメソッド、特異メソッドへのaliasも定義できます。 ```ruby # 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_*`を定義する構文も用意されています。 ```ruby # 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_*`も定義できます。 ```ruby # RBS class C # 特異クラスの attr_reader の定義 # Rubyだと以下 # class << self # attr_reader :foo # end attr_reader self.foo: Integer end ``` ### メソッドのオーバーロード `|`を使うとメソッドのオーバーロードを定義できます。 ```ruby # RBS class C # Integerを受け取ってStringを返すか、 # Stringを受け取ってIntegerを返すメソッド def foo: (Integer) -> String | (String) -> Integer # 3つ以上のオーバーロードも書ける def foo: (Integer) -> String | (Float) -> Rational | (String) -> Numeric end ``` `|`は慣習的にメソッド名の後ろのコロンの位置に揃えます。 ### エラーになるもの Rubyと違い、クラス・モジュール定義の外ではメソッドを定義できません。 ```ruby # RBS (構文エラー) def foo: () -> Integer ``` また、`class << self`構文を用いた特異メソッドの定義はできません。 ```ruby # RBS (構文エラー) class C class << self def foo: () -> Integer end end ``` 1つのクラス・モジュールに同名のメソッドを2回以上定義すると`rbs validate`でエラーになります。 ```ruby # RBS (RBS::DuplicatedMethodDefinitionError) class C def foo: () -> Integer def foo: () -> String end ``` ## インターフェイス RBSではRubyにない要素として、インターフェイスの定義ができます。 インターフェイスは`interface`キーワードで定義し、インターフェイス名は必ずアンダースコアで始まります。 ```ruby # RBS # 引数を受け取らずnilを返すfooメソッドを持つインターフェイスの定義 interface _Fooable def foo: () -> nil end class C # _Fooable を満たす値を1つ受け取るメソッド def bar: (_Fooable) -> nil end ``` インターフェイスはクラス/モジュールの中にも定義できます。 名前の衝突を避けるため、ライブラリが提供するインターフェイスはそのライブラリの名前空間の中に定義すると良いでしょう。 ```ruby # RBS module M interface _I end end class C # M::_I として定義したインターフェイスを参照できる。 def foo: () -> M::_I end ``` なお、インターフェイスの中にインターフェイスやクラス・モジュールを定義はできません。 ```ruby # RBS (構文エラー) interface _I class C end end ``` ## モジュールのinclude, extend モジュールの`include`, `extend`も同様に行なえます。 ```ruby # RBS module M end class C include M extend M end ``` またインターフェイスも同様に`include`, `extend`できます。 ```ruby # RBS interface _Fooable def foo: () -> Integer end class C include _Fooable extend _Fooable end ``` ## 型引数 RBSではクラス/モジュール/インターフェイスに、また個別のメソッド定義に型引数を使えます。 ### クラス・モジュール・インターフェイスに対して型引数を使う例 例えば簡単な`Array`クラスの定義と使用例は次のようになります。 ```ruby # RBS # Arrayの要素の型をElemとして定義したArrayクラス class Array[Elem] def first: () -> Elem end class C # String型を要素に持つArrayクラスのインスタンスを返すメソッド def foo: () -> Array[String] end ``` 複数の型引数も定義できます。次は簡単な`Hash`クラスの定義の例です。 ```ruby # 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`モジュールの定義と使用例は次のようになります。 ```ruby # RBS # _Eachable は別途定義する必要がある module Enumerable[Elem] : _Eachable def first: () -> Elem end class C # Stringに対してEnumerableが使えるクラス。 # C#first は String を返す。 include Enumerable[String] end ``` ### メソッドに対して型引数を使う例 メソッドに対して型引数を使う場合、メソッドの型の前に`[]`を書きます。 ```ruby # 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`構文を使って定義するメソッド名のエイリアスとは関係のない別の機能です。 ```ruby # RBS # String か Symbol のどちらかを表す別名として name を定義する例 type name = String | Symbol ``` また、Type Aliasはクラス・モジュールの中に定義することもできます。 ライブラリが提供するType Aliasは名前の衝突を避けるため、クラス・モジュールの中に定義すると良いでしょう。 ```ruby # RBS module M type name = String | Symbol end class C # モジュールMで定義したエイリアスnameを参照する例。 # このfooメソッドはStringかSymbolを返す。 def foo: () -> M::name end ``` ## 定数 RBSでは定数の型も定義できます。 ```ruby # RBS # トップレベルの定数Xの型をStringとして定義する X: String class C # 定数C::Xの型をStringとして定義する X: String end ``` ## インスタンス変数 RBSではインスタンス変数の型も定義できます。 ```ruby # RBS class C # @foo インスタンス変数の型をStringとして定義する @foo: String end ``` ## 型 ここまでで何の説明もなく`nil`や`Integer`を受け取る/返すと表現してきました。 `nil`や`Integer`を書いていたところには実際には色々な型を書くことが出来ます。 ここではそこに書くことができるものを紹介します。 ### Class Instance 一番良く使うのはクラスのインスタンスでしょう。単にクラス名を書くと、そのクラスのインスタンスを指す型となります。 ```ruby # 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`キーワードを使います。 ```ruby # RBS class C # Stringクラス自体を返すメソッド。 # Rubyだと def foo() String; end def foo: () -> singleton(String) end ``` ### リテラル型 一部のリテラルはRBSでもリテラルとして表現できます。 ```ruby # 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 ``` ### タプル 固定長の配列はタプル型として表現できます。 ```ruby # 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はレコード型として表現できます。 ```ruby # RBS class C # xがキーでIntegerが値のHashを返すメソッド。 # Rubyだと def foo1() { x: 42 } end def foo1: () -> { x: Integer } # ネストもできる def foo2: () -> { x: { y: { z: Integer} } } end ``` なお空のレコード型は書けません。 ```ruby # RBS (構文エラー) class C def foo: () -> { } end ``` ### Proc Procクラスのインスタンスを表す構文も用意されています。`^`の後ろにメソッド定義と同様のコードを書くと、Proc型となります。 ```ruby # 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`は継承先のクラスのインスタンスの型になります。 ```ruby # RBS class C def foo1: () -> self def foo2: () -> C end class D < C end ``` 例えばこの例の場合、`C#foo1`と`C#foo2`はどちらも`C`クラスのインスタンスを返すメソッドです。 一方`D#foo1`は`D`クラスのインスタンスを返すメソッドになります。`D#foo2`は`C#foo2`と変わらず、`C`クラスのインスタンスを返します。 #### nil, true, false それぞれ`NilClass`, `TrueClass`, `FalseClass`のインスタンスを表します。 `def foo: () -> nil`と`def foo: () -> NilClass`は等価です。 ただし前者がより推奨されています。つまり`NilClass`よりも`nil`のほうが推奨されています。 #### boolとboolish RBSは真偽値を示す型を2つ用意しています。`bool`と`boolish`です。 `bool`は`true | false`のエイリアスです。 一方`boolish`は全ての型のsupertypeであり、真偽値としてのみ使える型を表します。 全ての型の値を`boolish`型として宣言された変数に代入できますが、`boolish`型は真偽値以外の用途(メソッド呼び出しなど)に使うことは出来ません。 ```ruby # RBS class C # trueかfalseのみを受け取るメソッドの定義 def foo1: (bool) -> nil # どのような値でも受け取るメソッドの定義。 # ただし受け取った値は真偽値としてのみ使える def foo2: (boolish) -> nil end ``` #### void 使われない値であることを意味するときに使う型です。 ```ruby class C # 戻り値が使われないメソッドの定義 def foo: () -> void end ``` ### union 複数の型のどれか1つを表す型です。`|`で表します。 ```ruby # RBS class C # StringかIntegerを受け取ってStringを返すメソッドの定義 def foo: (String | Integer) -> String end ``` なお、戻り値の型にunion型を使う場合にはカッコでくくる必要があります。 これはオーバーロードとの区別をするためです。 ```ruby # RBS class C # StringかIntegerを返すメソッドの定義 def foo: () -> (String | Integer) # 以下は構文エラー def foo: () -> String | Integer end ``` ### intersection 複数の型の全てを兼ね備えた型です。`&`で表します。 ```ruby interface _Fooable def foo: () -> nil end interface _Barable def bar: () -> nil end class C # fooメソッドとbarメソッドの両方を持った型を受け取るメソッドの定義 def x: (_Foo & _Bar) -> nil end ``` ### optional `nil`かもしれない値を表す型です。型の後ろに`?`を後置して表します。 ```ruby class C # Integerかnilを返すメソッドの定義 def foo: () -> Integer? end ``` # 参考リンク この記事では駆け足でRBSの構文を紹介しました。 より詳しくRBSについて知りたい方は、次の資料が参考になるでしょう。 * https://github.com/ruby/rbs/blob/master/docs/syntax.md * 公式の構文についてのドキュメント * https://pocke.hatenablog.com/entry/2020/06/15/081130 * 私が書いたrbsコマンドについての解説記事 * ちょっと古いけど、だいたいの内容はrbs v1.0.0でも通用すると思います。 また実際のRBSを参考にしたい場合、[ruby/rbs](https://gtihub.com/ruby/rbs)の`core/`, `stdlib/`, `sig/`ディレクトリ下を見ると良いでしょう。 `core/`ディレクトリ下にはビルトインのライブラリのRBSが、`stdlib/`ディレクトリ下には標準添付ライブラリのRBSが、`sig/`ディレクトリ下にはRBS自体のRubyコードに対するRBSが含まれています。