はじめまして!
あしたのチームでエンジニアをしている吉岡です。
主にサーバーサイド側を担当しています。
今回は 昨年末にリリースされた Ruby3.0 に同梱された TypeProf
を使ってみたので、どんなことができたかをまとめてみました。
TypeProfとは??
ざっくり言うと 型注釈、型シグネチャがないRubyコードを静的型解析するためのツール
です。
TypeProfでできることは以下の2つです。
- 型エラーが起こる可能性がある箇所を検出できます
- 型シグネチャファイルのプロトタイプを生成できます
TypeProfを使うと何が嬉しい??
実は TypeProfを使わなくても Sorbet や Steep を使えば Rubyで型付けプログラミングをすることはできました。ではこのTypeProf、それらのツールと一体何が違うのでしょうか。それはこの一言につきます。
型注釈、型シグネチャファイルがなくても使える
あとでサンプルコードを載せていますが、 Steep などは、 .rbs という拡張子のファイルを用意し、そこに型情報を記載する必要があります。Sorbetの場合も同じ様な作業が必要です。
しかし、TypeProfではその必要がありません。今までと全く同じ開発方法のまま、型エラーがないかもチェックできる様になるなんて夢が広がります。
使ってみました
公式レポジトリのdemo.md に記載されているコード を TypeProf で解析してみます。
# test.rb class User attr_reader name: String attr_reader age: Integer def initialize: (name: String, age: Integer) -> void end def hello_message(user) "The name is " + user.name end def type_error_demo(user) "The age is " + user.age end user = User.new(name: "John", age: 20) hello_message(user) type_error_demo(user)
このファイルを解析して、rbs言語と言われる、他の型解析ツールで利用するツールのプロトタイプを生成します。
$ typeprof test.rb class Object private def hello_message: (User user) -> String def type_error_demo: (User user) -> untyped end class User attr_reader name: String attr_reader age: Integer def initialize: (name: String, age: Integer) -> [String, Integer] end
TypeProfによって生成された rbsファイルのプロトタイプを利用して、型のチェックを行うにはSteepというgemを使う様です。
SteepはRuby3.0に同梱されているわけではないので別途installする必要があります。
SteepでTypeProfを用いて作成したrbsファイルを使って解析するとこんな感じの結果が得られました。
$steep check # Type checking files: F. test.rb:4:2: [error] Cannot allow method body have type `::Array[(::String | ::Integer)]` because declared as type `[::String, ::Integer]` │ ::Array[(::String | ::Integer)] <: [::String, ::Integer] │ │ Diagnostic ID: Ruby::MethodBodyTypeMismatch │ └ def initialize(name:, age:) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ test.rb:14:18: [error] Cannot pass a value of type `::Integer` as an argument of type `::string` │ ::Integer <: ::string │ ::Integer <: (::String | ::_ToStr) │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ "The age is " + user.age ~~~~~~~~ Detected 2 problems from 1 file
1つめのエラーは実際のコード的には問題ないのですが、TypeProfによる推測が意図した通りに行われなかったためエラーとして検知されてしまった様です。
推測が上手く行われない場合は手動で編集する必要があるのでrbsファイルを編集します。
# test.rbs # Classes class Object private def hello_message: (User user) -> String def type_error_demo: (User user) -> String end # initializeの戻り値を void に変更 class User attr_reader name: String attr_reader age: Integer def initialize: (name: String, age: Integer) -> void end
$ steep check # Type checking files: F. test.rb:14:18: [error] Cannot pass a value of type `::Integer` as an argument of type `::string` │ ::Integer <: ::string │ ::Integer <: (::String | ::_ToStr) │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ "The age is " + user.age ~~~~~~~~ Detected 1 problem from 1 file
これでチェックに引っかかって欲しいところだけが引っかかる様になりました。この様に、生成したrbsプロトタイプを修正しないといけないケースがちょこちょこありそうです。
TypeProfを使った型エラー検知は、typeprof コマンドを実行時、 --show-errors(あるいはそのエイリアスの -v) というオプションをつければ行えます。
$ typeprof test.rb --show-errors # Errors test.rb:14: [error] failed to resolve overload: String#+ # Classes class Object private def hello_message: (User user) -> String def type_error_demo: (User user) -> untyped end class User attr_reader name: String attr_reader age: Integer def initialize: (name: String, age: Integer) -> [String, Integer] end
開発者のブログによると、TypeProf単体で型検知器として使うにはまだまだ問題が多いらしく、一旦rbsプロトタイプを生成する役割を担うとのことでした。
いずれSteepを使わず、TypeProfのみで正確な型チェックも行える様になるかもしれません。今後に期待です。
使ってみた感想
型注釈を付けなくても良いので、Rubyのバージョンを 3.0.0 に上げればすぐに使えるのはとても良いなと思いました!
Steepを使う場合、コード中に注釈を入れないといけなくなることもありそうですが、極力自分で型注釈を書かなくて良いと言うのは面白いなと思いました!
使っていく際、参考にさせていただいた記事
RubyKaigi 2019 "A Type-level Ruby Interpreter for Testing and Understanding" の発表要旨 Ruby 3 の静的解析ツール TypeProf の使い方 Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係についてのノート