Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

Ruby 3の型推論やってます

437 vues

Publié le

Ruby 3さみっと
https://rhc.connpass.com/event/169873/

Publié dans : Ingénierie
  • Identifiez-vous pour voir les commentaires

  • Soyez le premier à aimer ceci

Ruby 3の型推論やってます

  1. 1. Ruby 3の型推論やってます 遠藤 侑介 Ruby 3 さみっと 1
  2. 2. 雑談:endless新作 • ruby masterに新文法提案して仮採択されました • Endless method definition [Feature #16746] • この資料でもさっそく使っていきます! • と思ったけど、脳負荷がありそうなので控えめに 2 def foo = 42 def foo 42 end = と同じ
  3. 3. Ruby 3の型を1ページで Ruby 3は型記述言語・型推論・型検査を提供したい 3 def inc: (Integer) -> Integer ① 型記述言語 (RBS; ruby-signature) def inc(n) = n+1 コード ② 型推論 (ruby-type-profiler) ③ 型検査 (Steep, Sorbet, …) この発表では ②型推論を 話します
  4. 4. Ruby 3の型推論を1ページで • 型プロファイラ:型レベルのRubyインタプリタ • メソッドが受け取った型・返した型を集めて表示する 4 def foo(n) n.to_s end foo(42) Integer String def foo: (Integer) -> String
  5. 5. いい感じのデモ:ao.rb • 3Dレイトレーサ • 300行のトイプログラム • Syoyo Fujita, Hideki Miura作 • https://code.google.com/ archive/p/aobench/ • 若干改変あり 5
  6. 6. Demo: ao.rb 6 class Vec @x : Float @y : Float @z : Float initialize : (Float, Float, Float) -> Float x : () -> Float x= : (Float) -> Float ... vadd : (Vec) -> Vec vsub : (Vec) -> Vec vcross : (Vec) -> Vec vdot : (Vec) -> Float vlength : () -> Float vnormalize : () -> Vec 3Dベクトルのクラス ベクトル演算たち 座標 attr_accessor
  7. 7. Demo: ao.rb 7 class Scene @spheres : [Sphere, Sphere, Sphere] @plane : Plane initialize : () -> Plane ambient_occlusion : (Isect) -> Vec render : (Integer, Integer, Integer) -> Integer end class Sphere @center : Vec @radius : Float initialize : (Vec, Float) -> Float intersect : (Ray, Isect) -> (NilClass | Vec) end 3つの球 球は中心と半径
  8. 8. Demo: ao.rb 8 class Ray @org : Vec @dir : Vec initialize : (Vec, Vec) -> Vec org : () -> Vec dir : () -> Vec end class Isect @t : Float @hit : FalseClass | TrueClass @pl : Vec @n : Vec initialize : () -> Vec 光線は起点と方向 交点の判定・計算 交わるか否か boolean相当
  9. 9. Demo: ao.rb • いい感じでは? 9
  10. 10. アジェンダ • ➔型プロファイラの設計と性質 • 目標とアプローチ • 普通の型システムとの違い • デモと考察 • 型プロファイラの実装や工夫 • 今後 10
  11. 11. 型プロファイラの目標とアプローチ • 目的:型注釈なしRubyコードを静的解析する • 主目的:型記述(RBS)のプロトタイプを作る • 副目的:型エラーの可能性も指摘したい • アプローチ:抽象解釈ベースの解析 11 def foo(n) n.to_s end foo(42) Integer String def foo: (Integer) -> String
  12. 12. 普通の型システムとの違い • 普通の型システムはメソッド単位で解析する • 仮引数の型はどうする? • 手書きする(TypeScript他) • 我々の目的には合わない • 使われ方を見て決める(ML他) • 右のような例で難しい 12 class A def foo = 42 end class B def foo = "str" end def f(n) n.foo #=> 何? end
  13. 13. 型プロファイラの pros/cons pros •型注釈なしで解析可能 •型注釈なしで解析可能 •型注釈なしで解析可能 •型注釈なしで解析可能 •型注釈なしで解析可能 ※ただし~(後で) cons •解析が遅い •テストが必要 •誤推定はある •解析の理解が難しい •開発体験が未知 めちゃくちゃ挑戦的! 13
  14. 14. アジェンダ • 型プロファイラの設計と性質 • ➔デモと考察 • diff-lcs • ruby-type-profiler • 型プロファイラの実装や工夫 • 今後 14
  15. 15. 事例:diff-lcs • 最長共通部分列ライブラリ • ダウンロード数4位の人気gem→ • 簡単なテスト↓を起点に解析 15 https://bestgems.org/ require_relative "diff-lcs/lib/diff/lcs" class T; end Diff::LCS.diff([T.new]+[T.new], [T.new]+[T.new]) {}
  16. 16. diff-lcs解析結果(そこそこいい感じの例) 16 class Diff::LCS::Change include Comparable @element : NilClass | T | any @position : Integer | any @action : :+ | :- | any self.valid_action? : (:! | :+ | :- | :< | :== | :> | any) -> (FalseClass | TrueClass) action : () -> (String | any) position : () -> (Integer | any) element : () -> (NilClass | T | any) initialize : (String | any, Integer | any, NilClass | T | any) -> NilClass to_a : () -> ([String | any, Integer | any, NilClass | T | any]) unchanged? : () -> (FalseClass | TrueClass | any) end 列の中の要素 追加削除の位置 追加 or 削除 ※元コードはStringでしたがデモのためSymbolに書き換えた 誤推定っぽいのは薄くしてます
  17. 17. diff-lcs解析結果(難しい例) • 引数に依存して返り値の型が変わるメソッド • diff(ary, ary, DiffCallbacks) ➔ Array[Array[Diff::LCS::Change]] • diff(ary, ary, SDiffCallbacks)➔ Array[Diff::LCS::ContextChange] •オーバーロードのRBSは手書きしてください 17 module Diff::LCS self.diff : (Array[T] | Diff::LCS, Array[T] | any, ?NilClass) -> (Array[Array[Diff::LCS::Change | NilClass | any] | Diff::LCS::Change | Diff::LCS::ContextChange | NilClass | any] | any) end
  18. 18. diff-lcsで出た警告の例 • flow-sensitiveな解析が必要 ※実際にはもっといっぱい出てます 個別に原因究明して、修正や改善検討をする…… 18 if callbacks.respond_to?(:finished_a) and … … callbacks.finished_a(event) #=>「NilClass#finished_aを呼ぶかも」警告が出る … else
  19. 19. 事例:type-profiler • 型プロファイラのコード • Rubyで書かれている(5000行くらい)ので • 型プロファイルできる 19
  20. 20. type-profiler解析結果(いい感じの例) 20 class TP::Type include TP::Utils::StructuralEquality self.any : () -> TP::Type::Any self.bool : () -> TP::Type::Union self.nil : () -> TP::Type::Instance self.optional : (TP::Type | TP::Type::Any | TP::Type::Array | … | any) -> (TP::Type | TP::Type::Any | TP::Type::Array | … | any) self.guess_literal_type : (any) -> (TP::Type::Any | TP::Type::Array | … | TP::Type::Symbol) … end
  21. 21. type-profiler解析結果(難しい例) • 再帰構造がfalse positiveになる例 • ExecutionPoint (EP)は外側のEPへの参照を持つ 21 class TypeProfiler::ExecutionPoint … @outer : NilClass | TypeProfiler::ExecutionPoint | any # 一番外側のEPをたどるコード ep = EP.new(…) while ep.outer ep = ep.outer #=>「NilClass#outerを呼ぶかも」警告が出る end ep.pc #=>「NilClass#pcを呼ぶかも」警告が出る
  22. 22. type-profilerその他問題点(抜粋) • 巨大なUnionが出てきてつらい • 継承関係を利用してまとめる? • 型のエイリアスをうまく作る? • Object#method経由の呼び出しが追えない • 作り込みが足らない ※実際にはもっといっぱい(略) 22 (TP::Type | TP::Type::Any | TP::Type::Array | … | any)
  23. 23. FAQ • X | any は any と同じでは? • おっしゃるとおり • でもRBSプロトタイプ生成には便利なので あえて潰さずに残している 23
  24. 24. アジェンダ • 型プロファイラの設計と性質 • デモと考察 • ➔型プロファイラの実装や工夫 • 扱う型 • 分岐、可変長引数やキーワード引数 • ダミー実行や診断機能 • 今後 24
  25. 25. 扱う型:普通のやつ • 普通のインスタンス • NilClass、Integer、String、Objectなど • ユーザ定義クラス • 普通のクラスオブジェクト • クラスメソッド定義などで必要 • クラスのクラスや、特異クラスは扱わない • any型 • 未定義メソッド呼出、ダミー実行(後述)などで発生 • anyのメソッド呼び出し(any.foo())は無視する 25
  26. 26. 扱う型:コンテナ周り • 具体型:Symbol、リテラル型 • :key などは具体値として扱う • 整数 42 などは同一メソッド内では具体値として扱う • コンテナ型:配列とハッシュ • [X, Y, *Z]: 先頭はX型、次はY型、残りはZ型の配列 • [Integer, String]: 整数と文字列のタプル(ary[0]は先頭) • [*Integer]: 長さ不明の整数シーケンス • (RBSより表現力が少し強いので、RBSにするとき情報が落ちる) • {X=>Y, Z=>W}: キーがX型なら値はY型…、なハッシュ • {:key1=>Integer, Symbol=>String} 26
  27. 27. 扱う型:その他 • Proc型: ブロック、具体値として扱う • callやyieldをなるべく正確にトレースするため • Union型: 型の和集合の型 • (Integer | String) : 整数か文字列 • 工夫(というか制限) • ([*Integer] | [*String])は[*(Integer|String)]に潰す • 不正確だけど、組合せ爆発を避けて解析高速化を選んだ 27
  28. 28. 分岐の扱い • 両方実行して、Union型で合流する ※解析を確実に合流させるのは意外と難しく、ヒューリスティクスを使ってる(PCが小さい方から解析する) 28 x = nil if rand < 0.5 x = 42 else x = "str" end p(x) #=> Integer|String
  29. 29. 可変長引数とキーワード引数 • なんかそれっぽく動く(実装は地味に大変だった) 29 def foo(a, *r, z) r end foo(1, 2, 3) foo(1, 2, "S", 3) def foo: (Int, *Int|Str, Int) -> Array[Int|Str] def foo(n:, s:) { N: n, S: s } end foo(n: 42, s: "str") def foo: (n: Int, s: Str) -> {:N=>Int, :S=>Str}
  30. 30. 「ダミー実行」(1) • 解析が到達できないメソッドは解析できない • ダミー実行:未到達メソッドを無理やり実行する • 引数はすべてanyとする 30 def foo(n) n end def bar(a, b, c) foo(42) end fooもbarも テストがない ここにfooのヒントが あるけど使えない
  31. 31. 「ダミー実行」(2) • ダミー実行は良し悪し • ゴミ情報が波及することも 31 # ao.rb抜粋 class Vec # このメソッドは使われない def vadd(b) # b: any Vec.new( @x + b.x, # any @y + b.y, # any @z + b.z, # any ) end end class Vec @x: Float | any @y: Float | any @z: Float | any # 今はチートコードを追加(広義の型注釈?) if _ = false v = Vec.new(0.0,0.0,0.0) v.vadd(v) end vaddをanyで呼び出すので @xにanyが記録される 記録されない特殊な anyで解決できる?
  32. 32. 解析の診断機能 • pで型をrevealできる • 「ここにどんな型が来ると思ってる?」とか調べる • 解析到達経路をバックトレース風に表示する機能 32 def foo(n) p n end foo(1) foo("str") # Revealed types # reveal.rb:2 #=> Integer | String # Classes class Object foo : (Integer | String) -> (Integer | String) end
  33. 33. 困難に対して • 解析が遅い ➔ (精度を犠牲にしつつ)高速化してきた • テストが必要 ➔ (荒削りだけど)ダミー実行 • 誤推定・誤検出 ➔ (限界はあるけど)改善中 • 解析の理解が難しい ➔ (簡単だけど)診断機能 • 開発体験が未知 ➔ ? 33
  34. 34. アジェンダ • 型プロファイラの設計と性質 • デモと考察 • 型プロファイラの実装や工夫 • ➔今後 • 型プロファイラによるプログラミング体験 • 開発進捗と今後の予定 34
  35. 35. polyglot • 2つの言語で解釈可能なプログラム • 例:RubyとJavaScriptのpolyglot • コツ:片方の言語だけで解釈されるように頭を使う 35 if (0) print("Hello Ruby") [ここはRuby] else console.log("Hello JS") [ここはJS] // end
  36. 36. 型はpolyglot • 型付き言語のプログラムはpolyglot • インタプリタが動的な意味で解釈できる • さらに、型システムが静的な意味で解釈できる • 型注釈はチート(polyglotの観点では) • 型システムだけに指示を与えられるずるい記述 36
  37. 37. 型プロファイラの開発体験? • ずるくないpolyglot • 普通の実行に加え、型レベル実行を意識して書く • 別言語ではないのでそこまでつらくないと思う • それでもメリットはある(はず) • 型注釈なしの記述は疑いなくシンプル • 型プロファイラが解析できる≒素直な良いコード? 37 def foo(n: Integer | String) : Integer | String p n end foo(1) foo("str") def foo(n) p n end foo(1) foo("str") vs.
  38. 38. 進捗と今後 • 現状:やっとスタート地点 • 解析器の基本設計ができた • Rubyのおおよその言語機能がサポートできてきた • 組み込みクラスの知識をRBSから取り込んだ • 今後:実験と改善を繰り返す • バグの洗い出しと修正 • プログラミング体験の設計と不足機能の実装 • 診断機能、差分更新機能 • Railsアプリ解析用のドライバ開発 • など 38
  39. 39. まとめ • Ruby 3は型記述言語・型推論・型検査を提供し たい • 型推論担当の型プロファイラをやってます • (寛容な心で)手伝ってくれる人たのむ! • https://github.com/mame/ruby-type-profiler • ruby-jp slack の #types でも 39
  40. 40. 説明しなかったこと • オーバーロードの推定は諦めた • 爆発する • オーバーロードするときは基本的に手書きして • 再帰呼び出しはいい感じにできる • でも再帰的なデータ構造のハンドリングは微妙 • カスタムメソッド • 型プロファイラプラグイン • インスタンス変数の配列の破壊 • を説明するには、まずコンテナ型がメソッドを跨がらな いことを説明しないと…… 40

×