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

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

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

大阪Ruby会議02

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