Ruby 3の型解析
に向けた計画
遠藤 侑介
大阪Ruby会議02
1
自己紹介:遠藤侑介 (@mametter)
•クックパッドで働く
フルタイムRubyコミッタ
• テスト、CIの番人
• キーワード引数 粉砕 整理
• Ruby 3の静的解析
•クックパッドに興味あったら
お声がけください
2
余談:Ruby3キーワード引数の変更
Ruby2では「最後の引数=キーワード引数」だった
Ruby3ではこれらが禁止される予定!
• キーワード引数はキーワード引数として受け渡ししよう
• オプショナル引数はハッシュとして受け渡ししよう
3
def foo(kw: 1); end
foo(kw: 42)
def foo(kw:1); end
foo(**hash)
def foo(kw: 1)
end
foo({ kw: 42 })
def foo(kw: 1)
end
foo(hash)
余談:Ruby2.7のキーワード引数
•直すべきメソッド呼出しにレベル1警告が出ます
• 詳細な案内は追ってどこかに書きます
• まだ詳細な仕様が決まっていない(委譲を調整中)
• Railsはだいたい対応済み (!?)
4
def foo(kw: 1)
end
foo({ kw: 42 })
test.rb:3: warning: The last argument is used as the keyword parameter
test.rb:1: warning: for `foo' defined here
この発表では
•Ruby 3の静的解析の構想と進捗
•型プロファイラの設計と実装
• の、細かくて難しい面をはじめて話します
5
質問:型注釈 書きたいですか?
6
def increment: (Integer) -> Integer
def increment(n)
n + 1
end
ソースコード
型注釈
質問:型注釈 書きたいですか?
🙋
• 書きたくないし、他人にも書いてほしくない
• 書きたくないが、他人はどちらでもいい
• 書きたくないが、他人には書いてほしい
• 書きたい
7
Ruby 3の静的解析の構想
•目的: バグっぽいコードを指摘する
•要件: Rubyのプログラミング体験を維持する
(自分で)型を書かない選択肢を残したい
•構想
1. 標準の型シグネチャ言語
2. 型シグネチャなし型検査+シグネチャ推定
3. 型シグネチャあり型検査
8
1. 型シグネチャフォーマット(.rbs)
Rubyコードの型情報を示す標準形式
9
class Array[X] < Object
include Enumerable
def []: (Integer) -> X?
def []=: (Integer, X) -> X
def each: () { (X) -> void } -> Array[X]
...
end
組み込みメソッドの.rbsをRuby 3に同梱予定
コントリビューションチャンス!
github.com/ruby/ruby-signature
2. 型シグネチャなし検査+推定
無注釈コードの緩い型検査+型シグネチャ推定
def foo(n)
n + "s"
end
def bar(n)
ary = [1, "S"]
ary[n]
end
foo(gets.to_i)
bar(gets.to_i)
10
型プロファイラ開発中
github.com/mame/ruby-type-profiler
def bar:
(Int) -> (Int | Str)
TypeError: failed to
resolve Integer#+(String)
mruby 向けには mruby-meta-circular も
3. 型シグネチャあり型検査
型シグネチャとコードの整合性を検査する
class Foo
def foo(s)
s + 42
end
def bar(s)
s.gsuub(//,"")
end
end
class Foo
def foo:(Str)->Int
def bar:(Str)->Int
end
11
TypeError!
Str + Int
NoMethod
Error!
Steep github.com/soutaro/steep
Sorbet github.com/sorbet/sorbet
整合?
Ruby 3の方向性
•ライブラリ作者
.rbs 書いてください🙏
(型プロファイラの推定機能でサポートはしたい)
•アプリ作者
• 注釈書かず、検査もいらない → Ruby 2と同じ
• 注釈を書いてしっかり検査 → Steep/Sorbet等
• 注釈を書かず、緩く検査したい→型プロファイラ!
12
型プロファイラの話
13
型プロファイラとは
•目的:(アプリの)型シグネチャなしで
• ざっくり型エラーを探す
• 型シグネチャのプロトタイプを生成する
•方法:プログラムを型レベルで実行する
• ライブラリの型シグネチャ
• アプリのテスト
は必要
14
型プロファイラの動作イメージ
Rubyコードを「型レベル」で実行する
普通のインタプリタ
def foo(n)
n.to_s
end
foo(42)
Calls w/
42
Returns
"42"
型プロファイラ
def foo(n)
n.to_s
end
foo(42)
Calls w/
Integer
Returns
String
Object#foo ::
(Integer) -> String
15
型プロファイラと分岐
実行を「フォーク」する
def foo(n)
if n < 10
n
else
"error"
end
end
foo(42)
Fork!
イマココ
n<10 の真偽は
わからない
Object#foo ::
(Integer) ->
(Integer | String)
16
Returns
String
Returns
Integer
どのくらいできている?
•型プロファイラ自身が5秒で解析できる
• 2000行程度の普通のRubyコード
•optcarrotが3秒で解析できる
• 5000行程度の普通のRubyコード
17
例:ユーザ定義クラス
class Foo
end
class Bar
def make_foo
Foo.new
end
end
Bar.new.make_foo
Type
Profiler
Bar#make_foo :: () -> Foo
例:インスタンス変数
class Foo
attr_accessor :ivar
end
Foo.new.ivar = 42
Foo.new.ivar = "STR"
Foo.new.ivar
Type
Profiler
Foo#@ivar :: Integer | String
Foo#ivar= :: (Integer) -> Integer
Foo#ivar= :: (String) -> String
Foo#ivar :: () -> (String | Integer)
例:ブロック
def foo(x)
yield 42
end
s = "str"
foo(1) do |x|
s
end
Type
Profiler
Object#foo ::
(Integer, &Proc[(Integer) -> String])
-> String
例:再帰関数
def fib(n)
if n > 1
fib(n-1) + fib(n-2)
else
n
end
end
fib(10000)
Type
Profiler
Object#fib ::
(Integer) -> Integer
型プロファイラの何が難しいか?
•真似できる成功事例がない
抽象解釈・記号実行 長年研究されてきたが😟
•スケーラビリティと精度のトレードオフが難しい
型プロファイラは見逃しも誤検出も許容する
•実装がとにかく地味に大変
フルセットのRubyインタプリタ+型レベル評価設計
•「コンテナ型」が扱いづらい  今日のメイン
22
コンテナ型とは?
•配列やハッシュなど、他の型を要素に持つ型
• 配列を中心に説明します
•問題:型プロファイラで配列をどう扱うか?
23
a = [1, "str", true]
p a[0] #=> Integer
p a[1] #=> String
p a[2] #=> Boolean
解決策1:単なる「Array」型
•例えばIntegerの場合
•問題点:要素の型が出てくると破滅
24
n = 1 # Integer型
n.times {|i| } # Integer#timesとわかる
a = [1] # Array型
a[0] # 要素の型は不明(any型)
a[0].times {|i| } # 何もわからない
a[0].tmes {|i| } # 警告もできない
解決策2: ジェネリクス?
•要素の型を持つ型
•問題点:破壊的変更があると型が変わる!
a = [1] # Array<Integer>と推定
a[0] # Integerとわかる
a[0].times {|i| } # Integer#timesとわかる
a = [1] # Array<Integer>と推定
a << "str" # Array<Integer|String>に変わる!
解決策2: ジェネリクス?(続き)
•型プロファイラでは値レベルの区別がない
26
a = [1] # Array<Int>
b = [1] # Array<Int>(aと完全に同じ型?)
a[0] = "str" # Array<Int>がArray<Str>に変更??
b[0] # String??
a = [1] # Array<Int>
b = a # Array<Int>(aと同じ型)
a[0] = "str" # Array<Int>がArray<Str>に変更
b[0] # String
解決策3:値レベルの区別を入れる
•ナイーブにやると解析が有限時間で止まらない
• わりと研究中の分野(separation logic)
• 一方 Sorbet は型注釈を使った
27
a = T.let([1], T::Array[T.any(Integer, String)])
b = a
a[0] = "str"
b[0] # String | Integer
型プロファイラの現在の設計 (1)
配列ができた位置(allocation site)で区別
完璧ではないがわりとよくある妥協
28
1: a = [1] # Array<1行目> # 1行目→Int
2: b = [1] # Array<2行目> # 1行目→Int 2行目→Int
3: a[0] = "str" # 1行目→Str 2行目→Int
4: b[0] # Array<2行目>の要素はInt
1: a = [1] # Array<1行目> # 1行目→Int
2: b = a # Array<1行目> # 1行目→Int
3: a[0] = "str" # 1行目→Str
4: b[0] # Array<1行目>の要素はStr
設計 (1) の問題
29
1: class Foo
2: def to_a
3: [42] # Array<3行目>
4: end
5: end
6: a = Foo.new.to_a # Array<3行目>
7: b = Foo.new.to_a # Array<3行目>
8: a[0] = "str"
9: b[0] # String??
型プロファイラの現在の設計 (2)
メソッドは超える時は位置情報を失うとする
30
1: class Foo
2: def to_a(a)
3: [42] # Array<3行目>
4: end
5: end
6: a = Foo.new.to_a # Array<6行目>(3行目ではない)
7: b = Foo.new.to_a # Array<7行目>(3行目ではない)
型プロファイラの現在の設計 (3)
•タプル型:長さ固定、各要素を区別する
31
ary = [1, "str"] # [Int, Str]
ary[0] # 0番目はInt
ary[0].times {|i| } # IntなのでInt#timesとわかる
ary = [1] + ["str"] # Array[Int | Str]
ary[0] # Int | Str
ary[0].times {|i| } # Str#timesも呼ぶかも、と警告
•シーケンス型:長さ不明、全要素をまとめる
現在の設計 (3)
•リテラル型:元のリテラルの値を持つ型
32
0 # Literal<0, Int>
ary[0] # リテラル型なので0とわかる
def access(a, n)
a[n] # nはIntなので、aryのどこを読むかは不明
end
access([1, "STR"], 0) #=> Int|Str
# Literal<0, Int>だがメソッドには渡らない
リテラル型もメソッド境界は超えない
配列型の実装の細々した問題
型の領域が無限になる
• Array<Int>
• Array<Array<Int>>
• Array<Array<Array<Int>>>
• …
• 適当な深さで打ち切る予定
33
a = 42
while true
a = [a]
end
可変長引数のサポート
配列ができたら(一応)やるだけ
34
def foo(a, *r, z)
end
foo(1, 1, "str", 1)
foo:
(Int, Array<Int|Str>, Int)
-> NilClass
def foo(a, b, c)
end
ary = [42] + ["str"]
foo(*ary)
foo:
(Int|Str, Int|Str, Int|Str)
-> NilClass
余談:既知のバグ
35
def foo(a, b, c)
end
ary = [1]
foo(1, *ary, "str")
foo:(Int, Int, Str)->Nil
foo:(Int, Int|Str, Int|Str)->Nil
期待
実際
foo(1, *ary, "str")
foo(1, *(ary.dup+["str"]))
は
のように動く
理由:YARVバイトコードの実装の都合
関連研究
•mruby-meta-circular (Hideki Miura)
• 型プロファイラの元ネタ
•Type Analysis for JavaScript (Jensen, et al.)
•pytype (Google's unofficial project)
• 型解析のための抽象解釈器の事例
36
まとめ
•Ruby 3の静的解析の構想を説明しました
•型プロファイラの難しみ(コンテナ型)を
説明しました
• いろいろ悩みながらやってます
• 協力者募集中!
https://github.com/mame/ruby-type-profiler
37

Ruby 3の型解析に向けた計画