こんにちは!あしたのチームでクリエイティブ採用担当をしている貫井です!
最近メキメキと頭角を現している吉岡さんの「TypeProfを使ってみました」の記事が公開されました!
是非ご興味のある方は、お読みくださいませ!!
===============================================
はじめまして!
あしたのチームでエンジニアをしている吉岡です。
主にサーバーサイド側を担当しています。
今回は 昨年末にリリースされた Ruby3.0 に同梱された TypeProf を使ってみたので、どんなことができたかをまとめてみました。
TypeProfとは??
ざっくり言うと型注釈、型シグネチャがないRubyコードを静的型解析するためのツールです。
TypeProfでできることは以下の2つです。
- 型エラーが起こる可能性がある箇所を検出できます
- 型シグネチャファイルのプロトタイプを生成できます
- 現状、TypeProfが生成した型シグネチャを利用して、静的方解析が行えるツールには Steep などが挙げられます
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の関係についてのノート