Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

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

大阪Ruby会議02

Avatar for Yusuke Endoh

Yusuke Endoh

August 10, 2021
Tweet

More Decks by Yusuke Endoh

Other Decks in Programming

Transcript

  1. 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. 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. 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 整合?
  4. Ruby 3の方向性 •ライブラリ作者 .rbs 書いてください🙏 (型プロファイラの推定機能でサポートはしたい) • アプリ作者 • 注釈書かず、検査もいらない

    → Ruby 2と同じ • 注釈を書いてしっかり検査 → Steep/Sorbet等 • 注釈を書かず、緩く検査したい→型プロファイラ! 12
  5. 型プロファイラの動作イメージ 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
  6. 型プロファイラと分岐 実行を「フォーク」する 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
  7. 例:ユーザ定義クラス class Foo end class Bar def make_foo Foo.new end

    end Bar.new.make_foo Type Profiler Bar#make_foo :: () -> Foo
  8. 例:インスタンス変数 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)
  9. 例:ブロック def foo(x) yield 42 end s = "str" foo(1)

    do |x| s end Type Profiler Object#foo :: (Integer, &Proc[(Integer) -> String]) -> String
  10. 例:再帰関数 def fib(n) if n > 1 fib(n-1) + fib(n-2)

    else n end end fib(10000) Type Profiler Object#fib :: (Integer) -> Integer
  11. 解決策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| } # 警告もできない
  12. 解決策2: ジェネリクス? •要素の型を持つ型 • 問題点:破壊的変更があると型が変わる! a = [1] # Array<Integer>と推定

    a[0] # Integerとわかる a[0].times {|i| } # Integer#timesとわかる a = [1] # Array<Integer>と推定 a << "str" # Array<Integer|String>に変わる!
  13. 解決策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
  14. 型プロファイラの現在の設計 (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
  15. 設計 (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??
  16. 型プロファイラの現在の設計 (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行目ではない)
  17. 型プロファイラの現在の設計 (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も呼ぶかも、と警告 •シーケンス型:長さ不明、全要素をまとめる
  18. 現在の設計 (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>だがメソッドには渡らない リテラル型もメソッド境界は超えない
  19. 可変長引数のサポート 配列ができたら(一応)やるだけ 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
  20. 余談:既知のバグ 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バイトコードの実装の都合
  21. 関連研究 •mruby-meta-circular (Hideki Miura) • 型プロファイラの元ネタ • Type Analysis for

    JavaScript (Jensen, et al.) • pytype (Google's unofficial project) • 型解析のための抽象解釈器の事例 36