Slide 1

Slide 1 text

Ruby 3の型解析 に向けた計画 遠藤 侑介 大阪Ruby会議02 1

Slide 2

Slide 2 text

自己紹介:遠藤侑介 (@mametter) • クックパッドで働く フルタイムRubyコミッタ • テスト、CIの番人 • キーワード引数 粉砕 整理 • Ruby 3の静的解析 • クックパッドに興味あったら お声がけください 2

Slide 3

Slide 3 text

余談: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)

Slide 4

Slide 4 text

余談: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

Slide 5

Slide 5 text

この発表では • Ruby 3の静的解析の構想と進捗 •型プロファイラの設計と実装 • の、細かくて難しい面をはじめて話します 5

Slide 6

Slide 6 text

質問:型注釈 書きたいですか? 6 def increment: (Integer) -> Integer def increment(n) n + 1 end ソースコード 型注釈

Slide 7

Slide 7 text

質問:型注釈 書きたいですか? 🙋 • 書きたくないし、他人にも書いてほしくない • 書きたくないが、他人はどちらでもいい • 書きたくないが、他人には書いてほしい • 書きたい 7

Slide 8

Slide 8 text

Ruby 3の静的解析の構想 •目的: バグっぽいコードを指摘する • 要件: Rubyのプログラミング体験を維持する (自分で)型を書かない選択肢を残したい •構想 1. 標準の型シグネチャ言語 2. 型シグネチャなし型検査+シグネチャ推定 3. 型シグネチャあり型検査 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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 も

Slide 11

Slide 11 text

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 整合?

Slide 12

Slide 12 text

Ruby 3の方向性 •ライブラリ作者 .rbs 書いてください🙏 (型プロファイラの推定機能でサポートはしたい) • アプリ作者 • 注釈書かず、検査もいらない → Ruby 2と同じ • 注釈を書いてしっかり検査 → Steep/Sorbet等 • 注釈を書かず、緩く検査したい→型プロファイラ! 12

Slide 13

Slide 13 text

型プロファイラの話 13

Slide 14

Slide 14 text

型プロファイラとは •目的:(アプリの)型シグネチャなしで • ざっくり型エラーを探す • 型シグネチャのプロトタイプを生成する • 方法:プログラムを型レベルで実行する • ライブラリの型シグネチャ • アプリのテスト は必要 14

Slide 15

Slide 15 text

型プロファイラの動作イメージ 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

Slide 16

Slide 16 text

型プロファイラと分岐 実行を「フォーク」する 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

Slide 17

Slide 17 text

どのくらいできている? •型プロファイラ自身が5秒で解析できる • 2000行程度の普通のRubyコード •optcarrotが3秒で解析できる • 5000行程度の普通のRubyコード 17

Slide 18

Slide 18 text

例:ユーザ定義クラス class Foo end class Bar def make_foo Foo.new end end Bar.new.make_foo Type Profiler Bar#make_foo :: () -> Foo

Slide 19

Slide 19 text

例:インスタンス変数 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)

Slide 20

Slide 20 text

例:ブロック def foo(x) yield 42 end s = "str" foo(1) do |x| s end Type Profiler Object#foo :: (Integer, &Proc[(Integer) -> String]) -> String

Slide 21

Slide 21 text

例:再帰関数 def fib(n) if n > 1 fib(n-1) + fib(n-2) else n end end fib(10000) Type Profiler Object#fib :: (Integer) -> Integer

Slide 22

Slide 22 text

型プロファイラの何が難しいか? • 真似できる成功事例がない 抽象解釈・記号実行 長年研究されてきたが😟 • スケーラビリティと精度のトレードオフが難しい 型プロファイラは見逃しも誤検出も許容する •実装がとにかく地味に大変 フルセットのRubyインタプリタ+型レベル評価設計 • 「コンテナ型」が扱いづらい  今日のメイン 22

Slide 23

Slide 23 text

コンテナ型とは? •配列やハッシュなど、他の型を要素に持つ型 • 配列を中心に説明します •問題:型プロファイラで配列をどう扱うか? 23 a = [1, "str", true] p a[0] #=> Integer p a[1] #=> String p a[2] #=> Boolean

Slide 24

Slide 24 text

解決策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| } # 警告もできない

Slide 25

Slide 25 text

解決策2: ジェネリクス? •要素の型を持つ型 • 問題点:破壊的変更があると型が変わる! a = [1] # Arrayと推定 a[0] # Integerとわかる a[0].times {|i| } # Integer#timesとわかる a = [1] # Arrayと推定 a << "str" # Arrayに変わる!

Slide 26

Slide 26 text

解決策2: ジェネリクス?(続き) • 型プロファイラでは値レベルの区別がない 26 a = [1] # Array b = [1] # Array(aと完全に同じ型?) a[0] = "str" # ArrayがArrayに変更?? b[0] # String?? a = [1] # Array b = a # Array(aと同じ型) a[0] = "str" # ArrayがArrayに変更 b[0] # String

Slide 27

Slide 27 text

解決策3:値レベルの区別を入れる •ナイーブにやると解析が有限時間で止まらない • わりと研究中の分野(separation logic) • 一方 Sorbet は型注釈を使った 27 a = T.let([1], T::Array[T.any(Integer, String)]) b = a a[0] = "str" b[0] # String | Integer

Slide 28

Slide 28 text

型プロファイラの現在の設計 (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

Slide 29

Slide 29 text

設計 (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??

Slide 30

Slide 30 text

型プロファイラの現在の設計 (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行目ではない)

Slide 31

Slide 31 text

型プロファイラの現在の設計 (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も呼ぶかも、と警告 •シーケンス型:長さ不明、全要素をまとめる

Slide 32

Slide 32 text

現在の設計 (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>だがメソッドには渡らない リテラル型もメソッド境界は超えない

Slide 33

Slide 33 text

配列型の実装の細々した問題 型の領域が無限になる • Array • Array> • Array>> • … • 適当な深さで打ち切る予定 33 a = 42 while true a = [a] end

Slide 34

Slide 34 text

可変長引数のサポート 配列ができたら(一応)やるだけ 34 def foo(a, *r, z) end foo(1, 1, "str", 1) foo: (Int, Array, Int) -> NilClass def foo(a, b, c) end ary = [42] + ["str"] foo(*ary) foo: (Int|Str, Int|Str, Int|Str) -> NilClass

Slide 35

Slide 35 text

余談:既知のバグ 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バイトコードの実装の都合

Slide 36

Slide 36 text

関連研究 •mruby-meta-circular (Hideki Miura) • 型プロファイラの元ネタ • Type Analysis for JavaScript (Jensen, et al.) • pytype (Google's unofficial project) • 型解析のための抽象解釈器の事例 36

Slide 37

Slide 37 text

まとめ •Ruby 3の静的解析の構想を説明しました • 型プロファイラの難しみ(コンテナ型)を 説明しました • いろいろ悩みながらやってます • 協力者募集中! https://github.com/mame/ruby-type-profiler 37