2021年5月18日火曜日

Ruby3 で型入門

Ruby3 で型入門

概要

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 件のコメント:

コメントを投稿