$30 off During Our Annual Pro Sale. View Details »

Use Macro all the time ~ マクロを使いまくろ ~ 感想戦

osyo
September 15, 2021

Use Macro all the time ~ マクロを使いまくろ ~ 感想戦

Fukuoka.rb #226 - RubyKaigi 感想戦

osyo

September 15, 2021
Tweet

More Decks by osyo

Other Decks in Programming

Transcript

  1. Use Macro all the time
    ~ マクロを使いまくろ ~
    感想戦
    Fukuoka.rb #226 - RubyKaigi 感想戦

    View Slide

  2. 自己紹介
    osyo
    @pink_bangbi: https://twitter.com/pink_bangbi
    osyo-manga: https://github.com/osyo-manga
    Secret Garden(Instrumental): http://secret-garden.hatenablog.com
    Rails エンジニア
    好きな Ruby の機能は Refinements
    RubyKaigi は初参加
    Use Macro all the time ~ マクロを使いまくろ ~
    https://speakerdeck.com/osyo/use-macro-all-the-time-makurowoshi-imakuro-ri-ben-yu

    View Slide

  3. RubyKaigi の感想戦と補足

    View Slide

  4. 元々のモチベーション

    View Slide

  5. 元々のモチベーション
    元々はブロックから Ruby のコードを取得したい要求があった
    最初は iseq から位置情報を取得してファイルから読み込んできてた
    ただ、これだとパフォーマンス的な懸念点があった

    View Slide

  6. 元々のモチベーション
    元々はブロックから Ruby のコードを取得したい要求があった
    最初は iseq から位置情報を取得してファイルから読み込んできてた
    ただ、これだとパフォーマンス的な懸念点があった
    RubyVM::AST
    だとブロックから AST を取得できる実装を書いた
    ` `

    View Slide

  7. 元々のモチベーション
    元々はブロックから Ruby のコードを取得したい要求があった
    最初は iseq から位置情報を取得してファイルから読み込んできてた
    ただ、これだとパフォーマンス的な懸念点があった
    RubyVM::AST
    だとブロックから AST を取得できる実装を書いた
    これを利用するとマクロができるのでは…?
    この構想自体は1年ぐらい前からあった
    ` `

    View Slide

  8. 元々のモチベーション
    元々はブロックから Ruby のコードを取得したい要求があった
    最初は iseq から位置情報を取得してファイルから読み込んできてた
    ただ、これだとパフォーマンス的な懸念点があった
    RubyVM::AST
    だとブロックから AST を取得できる実装を書いた
    これを利用するとマクロができるのでは…?
    この構想自体は1年ぐらい前からあった
    知り合いに釣られて RubyKaigi に参加するモチベーションが湧いたので今回マクロを実
    装した
    RubyKaigi 駆動開発
    ` `

    View Slide

  9. Rensei の作業期間・苦労話

    View Slide

  10. Rensei の作業期間・苦労話
    作業期間は 3〜4ヶ月
    実際作業してたのは去年の今頃

    View Slide

  11. Rensei の作業期間・苦労話
    作業期間は 3〜4ヶ月
    実際作業してたのは去年の今頃
    Rensei は作業量とバグ修正が無限にあってつらかった
    100種類以上の AST を1つずつ実装していた

    View Slide

  12. Rensei の作業期間・苦労話
    作業期間は 3〜4ヶ月
    実際作業してたのは去年の今頃
    Rensei は作業量とバグ修正が無限にあってつらかった
    100種類以上の AST を1つずつ実装していた
    1 proc { |z, (a, b), c = 1, d = 2, *, (e, f, g), (h, i), j, k, l:, **kwd| foo }

    View Slide

  13. Rensei の作業期間・苦労話
    作業期間は 3〜4ヶ月
    実際作業してたのは去年の今頃
    Rensei は作業量とバグ修正が無限にあってつらかった
    100種類以上の AST を1つずつ実装していた
    1 proc { |z, (a, b), c = 1, d = 2, *, (e, f, g), (h, i), j, k, l:, **kwd| foo }
    実装した後も ActiveRecord のソースファイルを1つずつ食わせていくとバグが無限に発
    生して1つずつ直していった
    エッジケースの問題が大量にあった…

    View Slide

  14. Rensei の作業期間・苦労話
    作業期間は 3〜4ヶ月
    実際作業してたのは去年の今頃
    Rensei は作業量とバグ修正が無限にあってつらかった
    100種類以上の AST を1つずつ実装していた
    1 proc { |z, (a, b), c = 1, d = 2, *, (e, f, g), (h, i), j, k, l:, **kwd| foo }
    実装した後も ActiveRecord のソースファイルを1つずつ食わせていくとバグが無限に発
    生して1つずつ直していった
    エッジケースの問題が大量にあった…
    テストはかなり力を入れて書いた
    AST から復元したコードが元の AST と同じかどうかでテストしてる
    https://github.com/osyo-manga/gem-
    rensei/blob/82aaf139935a8b4eb7fd1029cdc5fc86e4fb692a/spec/unparser_spec.rb

    View Slide

  15. Rensei の実装に関して
    当日は時間がなかったので割愛したが以下のような感じで実装してる
    1 def unparse(node)

    2 case node&.type

    3 when :SCOPE

    4 unparse(node.children.last)

    5 when :LIT

    6 "#{node.children.last}"

    7 when :STR

    8 "#{node.children.last}"

    9 when :CALL

    10 recv, meth = node.children

    11 "#{unparse(recv)}.#{meth}"

    12 when :OPCALL

    13 left, op, args = node.children

    14 "(#{unparse(left)} #{op} #{unparse(args.children[0])})"

    15 end

    16 end

    17 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 * '42'.to_i")

    18 pp unparse(node) # => "(1 + (2 * 42.to_i))"

    View Slide

  16. Kenma の作業期間・苦労話

    View Slide

  17. Kenma の作業期間・苦労話
    作業期間 1〜2ヶ月
    今回の RubyKaigi に向けて実装した

    View Slide

  18. Kenma の作業期間・苦労話
    作業期間 1〜2ヶ月
    今回の RubyKaigi に向けて実装した
    実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した
    何回も実装を書き直しながら方向性が確定してから gem をつくりはじめた
    いろんな人に壁打ちしながらつくってた感謝

    View Slide

  19. Kenma の作業期間・苦労話
    作業期間 1〜2ヶ月
    今回の RubyKaigi に向けて実装した
    実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した
    何回も実装を書き直しながら方向性が確定してから gem をつくりはじめた
    いろんな人に壁打ちしながらつくってた感謝
    マクロのデザインに関してはかなり Rust を意識した
    マクロ関数に !
    付けたりとか
    ` `

    View Slide

  20. Kenma の作業期間・苦労話
    作業期間 1〜2ヶ月
    今回の RubyKaigi に向けて実装した
    実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した
    何回も実装を書き直しながら方向性が確定してから gem をつくりはじめた
    いろんな人に壁打ちしながらつくってた感謝
    マクロのデザインに関してはかなり Rust を意識した
    マクロ関数に !
    付けたりとか
    結果的に定義方法を種類分けしつつ、抽象的なマクロの定義ができて個人的には満足
    パターンマクロがかなり抽象的にかけてよい
    ` `

    View Slide

  21. RubyVM::AST
    の互換性
    ` `

    View Slide

  22. RubyVM::AST
    の互換性
    Ruby のバージョン間で互換性が保証されてない
    つらかったのは Ruby 2.6 -> 2.7 で :ARRAY -> :LIST
    にタイプ名が変わったところ
    ` `
    ` `

    View Slide

  23. RubyVM::AST
    の互換性
    Ruby のバージョン間で互換性が保証されてない
    つらかったのは Ruby 2.6 -> 2.7 で :ARRAY -> :LIST
    にタイプ名が変わったところ
    1 pp RUBY_VERSION # => "2.6.8"

    2 pp RubyVM::AbstractSyntaxTree.parse("[1, 2, 3]").children.last

    3 # => (ARRAY@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)
    ` `
    ` `

    View Slide

  24. RubyVM::AST
    の互換性
    Ruby のバージョン間で互換性が保証されてない
    つらかったのは Ruby 2.6 -> 2.7 で :ARRAY -> :LIST
    にタイプ名が変わったところ
    1 pp RUBY_VERSION # => "2.6.8"

    2 pp RubyVM::AbstractSyntaxTree.parse("[1, 2, 3]").children.last

    3 # => (ARRAY@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)
    1 pp RUBY_VERSION # => "2.7.4"

    2 pp RubyVM::AbstractSyntaxTree.parse("[1, 2, 3]").children.last

    3 # => (LIST@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)
    ` `
    ` `

    View Slide

  25. RubyVM::AST
    の互換性
    Ruby のバージョン間で互換性が保証されてない
    つらかったのは Ruby 2.6 -> 2.7 で :ARRAY -> :LIST
    にタイプ名が変わったところ
    1 pp RUBY_VERSION # => "2.6.8"

    2 pp RubyVM::AbstractSyntaxTree.parse("[1, 2, 3]").children.last

    3 # => (ARRAY@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)
    1 pp RUBY_VERSION # => "2.7.4"

    2 pp RubyVM::AbstractSyntaxTree.parse("[1, 2, 3]").children.last

    3 # => (LIST@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)
    どっちかって言うと細かいところでバグってるところのほうがつらかった
    Ruby の構文としては意味が異なるのに AST としては同じになるとか…
    proc { |a| }
    と proc { |a,| }
    が同じ AST になる とか
    https://bugs.ruby-lang.org/issues/17015
    ` `
    ` `
    ` ` ` `

    View Slide

  26. Rensei の AST 間のバージョン対応

    View Slide

  27. Rensei の AST 間のバージョン対応
    Rensei は現時点で存在してるバージョンはすべて対応している
    Ruby 2.6 ~ 3.1-dev

    View Slide

  28. Rensei の AST 間のバージョン対応
    Rensei は現時点で存在してるバージョンはすべて対応している
    Ruby 2.6 ~ 3.1-dev
    バージョン間で細かい非互換はあるけど基本的には新しい構文を追加するような実装に
    なっている

    View Slide

  29. Rensei の AST 間のバージョン対応
    Rensei は現時点で存在してるバージョンはすべて対応している
    Ruby 2.6 ~ 3.1-dev
    バージョン間で細かい非互換はあるけど基本的には新しい構文を追加するような実装に
    なっている
    詳しくは実装を見てね!!
    https://github.com/osyo-manga/gem-
    rensei/blob/82aaf139935a8b4eb7fd1029cdc5fc86e4fb692a/lib/rensei/unparser.rb

    View Slide

  30. マクロの今後

    View Slide

  31. マクロの今後
    エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたい
    ファイル全体ではなくて局所的にマクロを利用したい
    ユーザが定義したブロックでのみ使用するなど

    View Slide

  32. マクロの今後
    エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたい
    ファイル全体ではなくて局所的にマクロを利用したい
    ユーザが定義したブロックでのみ使用するなど
    例えばこんな感じ

    View Slide

  33. マクロの今後
    エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたい
    ファイル全体ではなくて局所的にマクロを利用したい
    ユーザが定義したブロックでのみ使用するなど
    例えばこんな感じ
    1 User.where { :age < 20 }

    View Slide

  34. マクロの今後
    エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたい
    ファイル全体ではなくて局所的にマクロを利用したい
    ユーザが定義したブロックでのみ使用するなど
    例えばこんな感じ
    1 User.where { :age < 20 }
    1 User.where("age < 20")

    View Slide

  35. マクロの今後
    エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたい
    ファイル全体ではなくて局所的にマクロを利用したい
    ユーザが定義したブロックでのみ使用するなど
    例えばこんな感じ
    1 User.where { :age < 20 }
    1 User.where("age < 20")
    マクロと言っているがどちらかというと AST というデータに対して今後フォーカスを当
    てて行くような未来が見えてきた気がする
    マクロでないにしても今後 AST を使って便利ななにかができてきそう

    View Slide

  36. おまけ

    View Slide

  37. おまけ
    Ruby 3.1 で RubyVM::AST::Node
    から元のコードが取得できるようになる(かも)
    ` `

    View Slide

  38. おまけ
    Ruby 3.1 で RubyVM::AST::Node
    から元のコードが取得できるようになる(かも)
    1 src = <<~EOS

    2 if hoge

    3 puts hoge + foo

    4 end

    5 EOS

    6
    7 # keep_script_lines
    を true
    にすると

    8 node = RubyVM::AbstractSyntaxTree.parse(src, keep_script_lines: true)

    9
    10 # #source
    メソッドでコードを取得できるようになる
    11 puts node.source

    12 # => if hoge

    13 # puts hoge + foo

    14 # end
    ` `

    View Slide