TypeProfを使ってみました

はじめまして!
あしたのチームでエンジニアをしている吉岡です。
主にサーバーサイド側を担当しています。

今回は 昨年末にリリースされた Ruby3.0 に同梱された TypeProf を使ってみたので、どんなことができたかをまとめてみました。

TypeProfとは??

ざっくり言うと 型注釈、型シグネチャがないRubyコードを静的型解析するためのツールです。
TypeProfでできることは以下の2つです。

  • 型エラーが起こる可能性がある箇所を検出できます
  • シグネチャファイルのプロトタイプを生成できます
    • 現状、TypeProfが生成した型シグネチャを利用して、静的方解析が行えるツールには Steep などが挙げられます

TypeProfを使うと何が嬉しい??

実は TypeProfを使わなくても SorbetSteep を使えば 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の関係についてのノート