概要
Ruby3 で導入された rbs, typeprof を試してみました 厳密には型を記述できるわけではなく型を事前に推論してくれる機能になります
環境
- macOS 11.3.1
- Ruby 3.0.0
- steep 0.44.1
テストコード
まずは普通に Ruby のコードを書きます このコールの引数や戻り値の型を抽出することができます
- vim user.rb
class User
def initialize(name, age)
@name = name
@age = age
end
def fake_name
@name + "_jose"
end
def fake_age
@age + 1
end
end
型を抽出してみる
定義したクラスの型を推論、抽出するには typeprof というコマンドを使います 試しに実行してみます
typeprof user.rb
# Classes
class User
@name: untyped
@age: untyped
def initialize: (untyped name, untyped age) -> untyped
def fake_name: -> untyped
def fake_age: -> untyped
end
型がわからなかった場合には untyped と表示されるようです どうやら実際にクラスのオブジェクトを生成してメソッドを実行していないと型判定できないようです
ちょっと修正: ちゃんとコールしたり、initizlize で返り値を設定する
先程の user.rb を実行するコードを追記してみます
vim user.rb
class User
def initialize(name, age)
@name = name
@age = age
end
def fake_name
@name + "_jose"
end
def fake_age
@age + 1
end
end
user = User.new("hawk", 10)
puts user.fake_name
puts user.fake_age
これで再度 typeprof してみるとちゃんと引数と戻り値に型が設定されています
typeprof user.rb
# Classes
class User
@name: String
@age: Integer
def initialize: (String name, Integer age) -> Integer
def fake_name: -> String
def fake_age: -> Integer
end
しかし initizlize の返り値がまだおかしいです 本来は User クラスのオブジェクトが返却されるべきですが Integer が返却されてしまっています
この場合は initialize で self を返すようにすれば OK です 以下のように修正しましょう
- vim user.rb
class User
def initialize(name, age)
@name = name
@age = age
self
end
def fake_name
@name + "_jose"
end
def fake_age
@age + 1
end
end
user = User.new("hawk", 10)
puts user.fake_name
puts user.fake_age
これで再度 typeprof するとちゃんと目的の型抽出ができるようになっていると思います
typeprof user.rb
# Classes
class User
@name: String
@age: Integer
def initialize: (String name, Integer age) -> User
def fake_name: -> String
def fake_age: -> Integer
end
これが typeprof の機能になります
rbs ファイルを生成する
次に rbs ファイルを生成してみます と言っても先程実行した typeprof の実行結果を rbs ファイルとして保存するだけです
- typeprof user.rb > user.rbs
rbs ファイルを使って型チェックを行う
steep というツールを使います グローバルインストールで良いと思います
- gem install steep
自分は mac + homebrew の Ruby を使っていたのですが以下も必要でした
- ln -s /usr/local/lib/ruby/gems/3.0.0/bin/steep /usr/local/opt/ruby/bin/steep
インストールできたら init します
- steep init
Steepfile という型チェックを行うルールを記載する DSL ファイルが作成されるので編集します
- vim Steepfile
target :app do
check "."
signature "."
end
check は user.rb があるディレクトリを指定し signature は user.rbs ファイルがあるディレクトリを指定します 今回はどちらも同じカレントディレクトリにあるのでカレントを指定します
これで実行してみましょう
- steep check
# Type checking files:
..........................................................
No type error detected. 🫖
こんな感じでエラーにならなければ OK です
引数の型を変更してエラーを発生させてみる
わざと型を間違えてエラーを発生させてみます user.rb を書き換えてみましょう
- vim user.rb
class User
def initialize(name, age)
@name = name
@age = age
self
end
def fake_name
@name + "_jose"
end
def fake_age
@age + 1
end
end
user = User.new(-1, 10)
puts user.fake_name
puts user.fake_age
本来は String である必要がある引数を -1 として Integer を指定してみます これで steep check を再度実行してみましょう
# Type checking files:
.............................F............................
lib/user.rb:17:16: [error] Cannot pass a value of type `::Integer` as an argument of type `::String`
│ ::Integer <: ::String
│ ::Numeric <: ::String
│ ::Object <: ::String
│ ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ user = User.new(-1, 10)
~~
Detected 1 problem from 1 file
こんな感じでエラーになるのが確認できると思います
使い所は
単純に引数や帰り値で型を明確に指定したいときには当然使えます
それ以外でぱっと思いつくところだとエディタ上で型チェックするときや CI に組み込んで型チェックするときかなと思います
vscode であれば steep と組み合わせて型チェックできるプラグインがあるようです https://github.com/soutaro/steep-vscode
最後に
rbs ファイルは自動生成してくれますが ruby スクリプトと側の記述によっては意図していない型定義を出力してしまう必要があることを考慮すると実質 rbs ファイルもメンテナンスしていく必要があるかなと思います
インタフェースを定義している感覚であればそこまで負担にはならないかなと思いますが規模が大きくなると当然コストも増えるかなと思います
すべてのクラスで rbs ファイルを作成しないで特定のクラスだけで使うのはありかもしれません
0 件のコメント:
コメントを投稿