Fukuoka.rb #226 - RubyKaigi 感想戦
Use Macro all the time~ マクロを使いまくろ ~感想戦Fukuoka.rb #226 - RubyKaigi 感想戦
View Slide
自己紹介osyo@pink_bangbi: https://twitter.com/pink_bangbiosyo-manga: https://github.com/osyo-mangaSecret Garden(Instrumental): http://secret-garden.hatenablog.comRails エンジニア好きな Ruby の機能は RefinementsRubyKaigi は初参加Use Macro all the time ~ マクロを使いまくろ ~https://speakerdeck.com/osyo/use-macro-all-the-time-makurowoshi-imakuro-ri-ben-yu
RubyKaigi の感想戦と補足
元々のモチベーション
元々のモチベーション元々はブロックから Ruby のコードを取得したい要求があった最初は iseq から位置情報を取得してファイルから読み込んできてたただ、これだとパフォーマンス的な懸念点があった
元々のモチベーション元々はブロックから Ruby のコードを取得したい要求があった最初は iseq から位置情報を取得してファイルから読み込んできてたただ、これだとパフォーマンス的な懸念点があったRubyVM::ASTだとブロックから AST を取得できる実装を書いた` `
元々のモチベーション元々はブロックから Ruby のコードを取得したい要求があった最初は iseq から位置情報を取得してファイルから読み込んできてたただ、これだとパフォーマンス的な懸念点があったRubyVM::ASTだとブロックから AST を取得できる実装を書いたこれを利用するとマクロができるのでは…?この構想自体は1年ぐらい前からあった` `
元々のモチベーション元々はブロックから Ruby のコードを取得したい要求があった最初は iseq から位置情報を取得してファイルから読み込んできてたただ、これだとパフォーマンス的な懸念点があったRubyVM::ASTだとブロックから AST を取得できる実装を書いたこれを利用するとマクロができるのでは…?この構想自体は1年ぐらい前からあった知り合いに釣られて RubyKaigi に参加するモチベーションが湧いたので今回マクロを実装したRubyKaigi 駆動開発` `
Rensei の作業期間・苦労話
Rensei の作業期間・苦労話作業期間は 3〜4ヶ月実際作業してたのは去年の今頃
Rensei の作業期間・苦労話作業期間は 3〜4ヶ月実際作業してたのは去年の今頃Rensei は作業量とバグ修正が無限にあってつらかった100種類以上の AST を1つずつ実装していた
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 }
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つずつ直していったエッジケースの問題が大量にあった…
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
Rensei の実装に関して当日は時間がなかったので割愛したが以下のような感じで実装してる1 def unparse(node)2 case node&.type3 when :SCOPE4 unparse(node.children.last)5 when :LIT6 "#{node.children.last}"7 when :STR8 "#{node.children.last}"9 when :CALL10 recv, meth = node.children11 "#{unparse(recv)}.#{meth}"12 when :OPCALL13 left, op, args = node.children14 "(#{unparse(left)} #{op} #{unparse(args.children[0])})"15 end16 end17 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 * '42'.to_i")18 pp unparse(node) # => "(1 + (2 * 42.to_i))"
Kenma の作業期間・苦労話
Kenma の作業期間・苦労話作業期間 1〜2ヶ月今回の RubyKaigi に向けて実装した
Kenma の作業期間・苦労話作業期間 1〜2ヶ月今回の RubyKaigi に向けて実装した実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した何回も実装を書き直しながら方向性が確定してから gem をつくりはじめたいろんな人に壁打ちしながらつくってた感謝
Kenma の作業期間・苦労話作業期間 1〜2ヶ月今回の RubyKaigi に向けて実装した実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した何回も実装を書き直しながら方向性が確定してから gem をつくりはじめたいろんな人に壁打ちしながらつくってた感謝マクロのデザインに関してはかなり Rust を意識したマクロ関数に !付けたりとか` `
Kenma の作業期間・苦労話作業期間 1〜2ヶ月今回の RubyKaigi に向けて実装した実装自体はそこまで難しくなかったが、とにかく書き心地を重視して API を設計した何回も実装を書き直しながら方向性が確定してから gem をつくりはじめたいろんな人に壁打ちしながらつくってた感謝マクロのデザインに関してはかなり Rust を意識したマクロ関数に !付けたりとか結果的に定義方法を種類分けしつつ、抽象的なマクロの定義ができて個人的には満足パターンマクロがかなり抽象的にかけてよい` `
RubyVM::ASTの互換性` `
RubyVM::ASTの互換性Ruby のバージョン間で互換性が保証されてないつらかったのは Ruby 2.6 -> 2.7 で :ARRAY -> :LISTにタイプ名が変わったところ` `` `
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.last3 # => (ARRAY@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)` `` `
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.last3 # => (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.last3 # => (LIST@1:0-1:9 (LIT@1:1-1:2 1) (LIT@1:4-1:5 2) (LIT@1:7-1:8 3) nil)` `` `
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.last3 # => (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.last3 # => (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` `` `` ` ` `
Rensei の AST 間のバージョン対応
Rensei の AST 間のバージョン対応Rensei は現時点で存在してるバージョンはすべて対応しているRuby 2.6 ~ 3.1-dev
Rensei の AST 間のバージョン対応Rensei は現時点で存在してるバージョンはすべて対応しているRuby 2.6 ~ 3.1-devバージョン間で細かい非互換はあるけど基本的には新しい構文を追加するような実装になっている
Rensei の AST 間のバージョン対応Rensei は現時点で存在してるバージョンはすべて対応しているRuby 2.6 ~ 3.1-devバージョン間で細かい非互換はあるけど基本的には新しい構文を追加するような実装になっている詳しくは実装を見てね!!https://github.com/osyo-manga/gem-rensei/blob/82aaf139935a8b4eb7fd1029cdc5fc86e4fb692a/lib/rensei/unparser.rb
マクロの今後
マクロの今後エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたいファイル全体ではなくて局所的にマクロを利用したいユーザが定義したブロックでのみ使用するなど
マクロの今後エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたいファイル全体ではなくて局所的にマクロを利用したいユーザが定義したブロックでのみ使用するなど例えばこんな感じ
マクロの今後エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたいファイル全体ではなくて局所的にマクロを利用したいユーザが定義したブロックでのみ使用するなど例えばこんな感じ1 User.where { :age < 20 }
マクロの今後エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたいファイル全体ではなくて局所的にマクロを利用したいユーザが定義したブロックでのみ使用するなど例えばこんな感じ1 User.where { :age < 20 }1 User.where("age < 20")
マクロの今後エンドユーザがマクロを使うと言うよりかは間接的にマクロが利用できるようにしたいファイル全体ではなくて局所的にマクロを利用したいユーザが定義したブロックでのみ使用するなど例えばこんな感じ1 User.where { :age < 20 }1 User.where("age < 20")マクロと言っているがどちらかというと AST というデータに対して今後フォーカスを当てて行くような未来が見えてきた気がするマクロでないにしても今後 AST を使って便利ななにかができてきそう
おまけ
おまけRuby 3.1 で RubyVM::AST::Nodeから元のコードが取得できるようになる(かも)` `
おまけRuby 3.1 で RubyVM::AST::Nodeから元のコードが取得できるようになる(かも)1 src = <<~EOS2 if hoge3 puts hoge + foo4 end5 EOS67 # keep_script_linesを trueにすると8 node = RubyVM::AbstractSyntaxTree.parse(src, keep_script_lines: true)910 # #sourceメソッドでコードを取得できるようになる11 puts node.source12 # => if hoge13 # puts hoge + foo14 # end` `