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

†Ruby黒魔術経典†

 †Ruby黒魔術経典†

名古屋Ruby会議04 発表資料

Tomohiro Hashidate

June 08, 2019
Tweet

More Decks by Tomohiro Hashidate

Other Decks in Programming

Transcript

  1. †Ruby
    黒魔術経典†
    @joker1007 (Repro inc. CTO)
    名古屋Ruby
    会議04

    View Slide

  2. self.inspect
    @joker1007
    Repro inc. CTO
    Data Engineering, Architect
    Ruby
    で悪事を働く⼈と⾒做されている

    View Slide

  3. We provide a web service as ...
    Analytics of Mobile Apps, Web Apps.
    Marketing Automation.
    We're hiring!!

    View Slide

  4. https://youtu.be/1mauqP9zWbM?t=31

    View Slide

  5. ⼤いなる⼒には
    ⼤いなる責任が
    伴う

    View Slide

  6. メタプログラミングや魔術的な挙動をする
    コードは強⼒だが
    侵してはならないこともある

    View Slide

  7. 今⽇の議題
    以下の内容について話す。
    黒魔術において守るべきルール
    実際に魔術を編み出すために使えるパーツ
    考え⽅の具体例

    View Slide

  8. Part1
    守りの黒魔術

    View Slide

  9. 守りの黒魔術 その1
    組込みクラスを壊さない
    特に
    require
    しただけで挙動を変えない
    上書きさせたい場合は、必ずユーザーに指定させる
    組込みのクラス・メソッドは全てのRuby
    ユーザーが期待している挙動が
    ある。(
    当たり前の話)
    暗黙的に弄って挙動を変えたら、何が起こるのか分からなくなる。

    View Slide

  10. 守りの黒魔術 その2
    スタックが追える様にすること
    eval
    を使う場合に定義場所のロケーションを適切に設定する。
    でないと例外が発⽣した時にどこで定義されたコードなのか分からない。
    実はRuby
    の例外のバックトレースは⾃分で上書きできるので、ノイズに
    なりそうな情報を隠すこともできる。

    View Slide

  11. 守りの黒魔術 その3
    パフォーマンスを意識すること
    eval
    やbinding
    は負荷が⾼い。
    呼び出し回数を少なくするために以下の様な⽅法が使える。
    クラスレベルでソースコードをキャッシュする
    クラス定義時や初回実⾏時のみ動作する様にする
    メソッド定義にだけ利⽤する
    初回実⾏時にメソッドを上書きする

    View Slide

  12. 守りの黒魔術 その4
    TracePoint
    の利⽤は明⽰的に
    そして、絶対にensure
    でdisable
    すること。
    途中で例外が発⽣するとtrace
    が有効のままになる。
    trace
    が有効のままになるとマジで何が起きるか分からない。
    フックが暴⾛してstack level too deep
    になるのはまだ良い⽅。

    View Slide

  13. Part2
    黒魔術に使えるAPI
    の探し⽅

    View Slide

  14. 基本はリファレンスをひたすら読むこと
    それだけだと雑過ぎるので、もうちょい解説する。

    View Slide

  15. るりまのここを読むべし
    BasicObject
    Object
    Module/Class
    Method
    Proc
    Kernel
    (ObjectSpace)

    View Slide

  16. ⼀回読んでも忘れるから、
    とりあえずこの辺りを読み返す癖を付けておく。
    ちなみに、 RubyVM::AbstractSyntaxTree
    はまだるりまが無いです。
    (
    プルリクチャンス)

    View Slide

  17. 使えそうな機能の例
    評価コンテキスト操作
    eval

    トリガー/
    フック
    メソッドフック, TracePoint, included, inherited,
    method_missing, trace_var, trap, finalizer
    ⼤域脱出
    throw/catch, Fiber

    View Slide

  18. 使えそうな機能の例 (
    続き)
    オブジェクト参照
    _variable_get
    系, const_get, ObjectSpace
    変数/
    定数操作
    _variable_set
    系, const_set
    メタデータ取得
    Method
    やProc
    から取れる情報
    メソッドの動的定義
    define_method, module_eval

    View Slide

  19. メタプログラミングパターン
    メソッド定義
    DSL
    動的解析
    静的解析
    ⾃動的/
    暗黙的処理の追加
    ⾔語拡張

    に向かう程魔術度が増す

    View Slide

  20. Part3
    考え⽅の具体例
    暗黙のブロック引数 it
    を作ってみよう
    see. https://bugs.ruby-lang.org/issues/15897

    View Slide

  21. proc
    の中で暗黙の内に it
    でパラメータを参照できるとは、
    Ruby
    の動作に置き換えるとどういうことかを考えてみる。
    評価コンテキスト内で it
    というローカル変数に値が⼊っている
    proc
    のself
    に it
    というメソッドが定義されていて、パラメータを取
    得できる
    proc
    の外側で it
    が定義されている。
    ローカル変数 or
    引数
    これらのどれかが実現できれば良さそう。

    View Slide

  22. ローカル変数追加⽅式について検討
    local_variable_set
    が使えそう
    local_variable_set
    は変数書き換えは簡単だが、新規に追加するのは難
    しい
    binding
    が毎回新しく⽣成されるため
    binding
    を固定してeval
    しなければならない
    そもそもブロック呼び出しに実際に使われている値をどうやって事前
    に取得する?
    なんか無理っぽい

    View Slide

  23. メソッド追加⽅式について検討
    評価コンテキストにおけるself
    は取得できる
    Proc#binding
    やTracePoint
    で可能
    単純にメソッドを定義するとあるクラスのインスタンス全てが影響す

    特異メソッドとして定義すれば可能かも
    しかしスコープを抜けた後も参照できてしまう
    Refinements
    は使えないか
    ブロックの定義が別の場所なのでeval
    が必要
    ローカル変数⽅式と同様の問題がある

    View Slide

  24. 外側でit
    を定義する⽅法について検討
    新しいproc
    でラップして追加できる
    ブロックに渡される引数を事前に知る必要が無い
    やはりeval
    する必要がある

    View Slide

  25. 結論:
    恐らくeval
    が必須
    そしてproc
    でwrap
    ⽅式が現実的
    eval
    するためにはソースコード断⽚が必要

    View Slide

  26. ブロックのソースコードを取る⽅法
    RubyVM::AST.of or parser gem
    で位置を特定し読む
    (
    またお前か)

    View Slide

  27. 特定のメソッドを対象にPoC
    を書く
    module Ext
    def map(*args, &block)
    source = File.readlines(block.source_location[0])
    proc_binding = block.binding
    ast = RubyVM::AbstractSyntaxTree.of(block)
    args_tbl = ast.children[0]
    block_node = ast.children[2]
    if args_tbl.empty?
    extracted = extract_source(
    source,
    block_node.first_lineno,
    block_node.first_column,
    block_node.last_lineno,
    block_node.last_column)
    new_block = proc_binding.eval("proc { |it| #{extracted} }")
    super(*args, &new_block)
    else
    super(*args, &block)
    end
    end
    end
    Array.prepend(Ext)
    n = 3
    [1, 2, 3].map { p it + n }
    # => [4, 5, 6]

    View Slide

  28. 出来た!
    後はgem
    にするだけ

    View Slide

  29. こんな感じで、⾃分の場合はゴールから逆算して考える。
    やりたい事が出来るとはRuby
    においてオブジェクトの状態や変数のスコ
    ープ、メソッドの定義がどうなっていればいいかを想像し、そこに⾄る⽅
    法を逆向きに辿って実現可能な⽅法を考える。

    View Slide

  30. ちなみに、実はこれ
    RubyKaigi2019
    で話したものと同じ
    パターンを使っている

    View Slide

  31. 最後に
    黒魔術を使うためにはRuby
    の挙動や各オブジェクトが何なのかというこ
    とを詳しく知る必要がある。
    魔術的な挙動を起こす⽅法を知ることは、安全なコードの書き⽅を知るこ
    とにも繋がる。
    いざという時の選択肢も増える。
    Ruby
    より深く楽しみ、より良いコードに繋げよう

    View Slide