# 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が含まれています。