Ruby 3 の静的解析ツール TypeProf の使い方

こんにちは、フルタイムRubyコミッタとして働いてる遠藤(@mametter)です。昨日、Ruby 3.0.0-preview2がリリースされました!

このリリースには、遠藤が開発している Ruby の静的型解析ツール TypeProf が初めて同梱されています。これの使い方をかんたんにご紹介したいと思います。

デモ

TypeProf は、型注釈のない Ruby コードを無理やり型解析するツールです。とりあえずデモ。

# user.rb
class User
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  attr_reader :name, :age
end

User.new(name: "John", age: 20)

typeprof コマンドは、Ruby 2.7 で gem install typeprof でインストールできます *1 。TypeProf にこの Ruby コードを与え、typeprof user.rb -o user.rbs と実行してください。次のような内容の user.rbs が生成されているはずです。

# Classes
class User
  attr_reader name: String
  attr_reader age: Integer
  def initialize: (name: String, age: Integer) -> Integer
end

TypeProf は、与えられた Ruby コードの型情報を推定して出力します。user.rbs は、Ruby ではなく、RBS という Ruby 3 標準の型情報記述の言語で書かれています。雰囲気でなんとなく読めるかと思いますが、たとえば def initialize: (name: String, age: Integer) -> Integername というキーワード引数が String インスタンスを受け取り、age というキーワード引数が Integer インスタンスを受け取る、ということを表現しています。返り値は Integer ですが、initialize の返り値はあまり意味がないですね。

TypeProf の特徴は、メソッド呼び出しの情報をフル活用するところです。これは従来のふつうの型解析と本質的に異なるところです。これにより、def initialize(name:, age:) ... end という型注釈が一切ない定義に対しても、User.new(name: "John", age: 20) という呼び出しで渡される型を見てそれっぽい型情報を推定します。

もうひとつデモ

TypeProf は、Ruby コードだけでなく RBS も合わせて解析できます。デモ。

# test.rb
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)
# user.rbs
class User
  def initialize: (name: String, age: Integer) -> void

  attr_reader name: String
  attr_reader age: Integer
end

user.rbs は、前の例の出力を少しだけ手修正したものです。これと test.rb をあわせて解析します。 typeprof -v test.rb user.rbs と実行してください。

# Errors
test.rb:7: [error] failed to resolve overload: String#+

# Classes
class Object
  private
  def hello_message: (User) -> String
  def type_error_demo: (User) -> untyped
end

コマンドライン引数の -v はエラーの可能性を表示させるオプションです。 このため、今度は # Errors という出力があります。 test.rb の 7 行目を見てみると、"The age is " + user.age という計算をしていますが、これは StringInteger を結合しようとしています。 TypeProf はこれをバグとして警告しています。

class Object から end は、先程と同様に test.rb の型情報を推定したものです。 なお、7 行目に型エラーの可能性があって型の追跡ができなくなったため、type_error_demo メソッドの返り値は untyped となってます。

Ruby TypeProf Playground

TypeProf は Ruby TypeProf Playground でブラウザ上で試せます *2

左上が Ruby コード、左下が RBS(書かなくても良い)で、Analyze ボタンを押すと右側に解析結果が表示されます。 Ruby コードをいじって解析することもできるので、期待に反する挙動を見つけたら Report bug ボタンでぜひ報告してください *3

TypeProf の課題と現状

TypeProf は「型注釈を書かない選択肢を Ruby に残す」ということを至上命題とした極端な設計になっているので、様々な問題点もあります。

  • とにかく解析が遅い *4
  • まともな解析のためにはテストコードが必要 *5
  • 現時点では解析精度が低く、誤検知や見逃しがとても多い
  • 手本になる前例がなく、TypeProf 自体の設計に試行錯誤が必要

そのため Ruby 3.0 の TypeProf では、型注釈のない Ruby コードに対して RBS スタブを生成する、という機能にフォーカスして設計・実装を進めました *6 。 RBS スタブ生成機能として経験を積みながら、高速化や解析精度向上を進め、将来的にはかんたんな型検査器として使えるものにできたらいいなと思っています。

まとめ

Ruby 3 に同梱される型解析ツール TypeProf をご紹介しました。かんたんな使い方にフォーカスして書いたので、もう少し詳しいことはドキュメントや過去の発表をご参照ください。

github.com

rubykaigi.org

TypeProf の現状の完成度としては、Ruby パッケージに含まれるすべての .rb ファイルで解析が通る *7 ことを確認できた程度です。出力の精度評価や速度向上はまだまだこれから頑張っていきます。ぜひ遊んでみて、気づいたことがあったらバグ報告でも感想でもいただけると泣いて喜びます。

RBS と TypeProf の関係は? Steep や Sorbet というのも聞いたが?

Ruby 3 の静的解析は固有名詞が多くてややこしいので、関係を別記事にまとめました。

techlife.cookpad.com

*1:ruby 3.0.0-preview2 なら gem install なしで typeprof コマンドが利用可能です。

*2:社内で「こういうのを作りたい」と語ったら、id:koba789 さんが 1 時間で作ってくれました。

*3:とても適当に運用しているので、サーバが落ちたらごめんなさい。

*4:ふつうの型解析は基本的にメソッド単位で解析を行いますが、TypeProf は呼び出し元をたどる必要があるため、大変になりがちです。なお、RBS 言語を部分的に手書きすればするほど(理論上は)早くなります。

*5:解析のカバレッジを上げるため、スタブ実行というヒューリスティクスを実装しています。これは、どこからも呼ばれなかったメソッドに untyped な引数を与えて無理やり呼び出すものです。これにより、テストがなくても一通りの解析は行えるようになっています。

*6:なお、rbs コマンドにも Ruby コードから RBS スタブを生成する機能がありますが、こちらは基本的にすべての引数を untyped として出力するものです。やることが単純な分、速くて安定しているというメリットもあります。

*7:TypeProf が理不尽に例外終了しないことを確かめた程度ですが、やんちゃなコードだらけの test/ruby や ruby/spec はそれだけでも地獄の苦しみでした。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/