1.2K Views
December 23, 21
スライド概要
https://rubykaigi.org/2021-takeout/presentations/pink_bangbi.html
Ruby / Vim / C++
Use Macro all the time ~ マクロを使いまくろ ~ RubyKaigi Takeout 2021
やあ、みんな おはよう こんにちわ こんばんわ
みんな Ruby を使ってい る???
Ruby を使っていると…
1 CONST_VALUE = [1, 2, 3] こういう定数定義を
1 CONST_VALUE = [1, 2, 3] こういう定数定義を 1 CONST_VALUE = [1, 2, 3].freeze 暗黙的に `freeze` させたり
1 puts config.hoge_flag 2 puts config.foo_flag みたいなデバッグ出力を
1 puts config.hoge_flag 2 puts config.foo_flag みたいなデバッグ出力を 1 # output: 2 "config.hoge_flag # => true" 3 "config.foo_flag # => false" みたいに出力内容と出力結果を一緒に出力させたり
1 ![a, b, c] こういうコードを
1 ![a, b, c] こういうコードを 1 { a: a, b: b, c: c } みたいに Hash で展開させたりとか
やりたくなりますよね!!
それマクロでできるよ!!!!
自己紹介 名前:osyo Twitter : @pink_bangbi https://twitter.com/pink_bangbi github : osyo-manga https://github.com/osyo-manga ブログ : Secret Garden(Instrumental) http://secret-garden.hatenablog.com Rails エンジニア 好きな Ruby の機能は Refinements RubyKaigi は初参加 10 / 89
今日話すこと 11 / 89
今日話すこと Ruby でマクロを実装した話 11 / 89
アジェンダ Ruby のマクロとは AST とは マクロの変換プロセスの解説 Rensei - 錬成 - AST から Ruby のコードを生成するライブラリ Kenma - 研磨 - 任意の AST を別の AST に変換するライブラリ マクロの使用例 これからの課題 12 / 89
マクロとは? 13 / 89
マクロとは 14 / 89
マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる 14 / 89
マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる この登壇では『Ruby の AST を別の AST に変換すること』を『Ruby のマクロ』と定義 14 / 89
マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる この登壇では『Ruby の AST を別の AST に変換すること』を『Ruby のマクロ』と定義 この『マクロ』を使用すると Ruby のコードを構文レベルで変更する事ができる 例えば `hoge.foo` を `hoge&.foo` に変更したり 理論上は valid な Ruby のコードであればどんなコードにでも変換できる 14 / 89
そもそも AST って? 15 / 89
AST とは 16 / 89
AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 16 / 89
AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 今回は `RubyVM::AbstractSyntaxTree` を使用する で使用される実データは `RubyVM::AbstractSyntaxTree::Node` だが、 このスライドでは一部配列形式で記述している 以下 `RubyVM::AST::Node` と略 `RubyVM::AbstractSyntaxTree` 16 / 89
AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 今回は `RubyVM::AbstractSyntaxTree` を使用する で使用される実データは `RubyVM::AbstractSyntaxTree::Node` だが、 このスライドでは一部配列形式で記述している 以下 `RubyVM::AST::Node` と略 `RubyVM::AbstractSyntaxTree` 抽象化されたデータ構造なので異なるコードでも同じ AST になることがある 例えば `cond ? foo : bar` と `if cond; foo; else bar; end` は同じ AST になる 16 / 89
AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 今回は `RubyVM::AbstractSyntaxTree` を使用する で使用される実データは `RubyVM::AbstractSyntaxTree::Node` だが、 このスライドでは一部配列形式で記述している 以下 `RubyVM::AST::Node` と略 `RubyVM::AbstractSyntaxTree` 抽象化されたデータ構造なので異なるコードでも同じ AST になることがある 例えば `cond ? foo : bar` と `if cond; foo; else bar; end` は同じ AST になる AST の種類は構文ごとに細かく分かれていて100種類以上ある 16 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
6
7
pp node
pp node.type
pp node.children
8
9
10
11
12
オブジェクトを渡すとブロックの中身の
を返す
node2 = node.children.last
pp node2
pp node2.type
pp node2.children
17 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
`RubyVM::AbstractSyntaxTree.parse`
1
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
2
3
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
pp node.type
pp node.children
オブジェクトを渡すとブロックの中身の
を返す
で `1 + 2` の AST を取得する
8
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
17 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
`RubyVM::AbstractSyntaxTree.parse`
1
2
3
4
5
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
6
7
pp node.type
pp node.children
オブジェクトを渡すとブロックの中身の
pp node
を返す
で `1 + 2` の AST を取得する
`.of` で `Proc` から取得することもでき
る
8
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
17 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
1
2
3
`RubyVM::AbstractSyntaxTree.parse`
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
オブジェクトを渡すとブロックの中身の
4
5
6
7
pp node
pp node.type
pp node.children
8
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
[出力結果]
1
2
3
4
を返す
で `1 + 2` の AST を取得する
`.of` で `Proc` から取得することもでき
る
取得した AST のデータはこのようになっ
ている
これが `RubyVM::AbstractSyntaxTree::Node`
のデータ形式
(SCOPE@1:0-1:5
tbl: []
args: nil
body: (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)))
17 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
8
オブジェクトを渡すとブロックの中身の
を返す
AST は `type` と `children` の2つの情報
を持っており、これが木構造になっている
pp node.type
pp node.children
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
[出力結果]
1
2
3
4
:SCOPE
[[],
nil,
(OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil))]
18 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
8
オブジェクトを渡すとブロックの中身の
pp node.type
pp node.children
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
を返す
AST は `type` と `children` の2つの情報
を持っており、これが木構造になっている
大枠に `SCOPE` という AST があり、その
下に `1 + 2` の AST がぶら下がっている
[出力結果]
1
2
3
4
:SCOPE
[[],
nil,
(OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil))]
18 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
の AST を取得する場合は `SCOPE`
の子から取得する
`1 + 2`
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
pp node.type
pp node.children
オブジェクトを渡すとブロックの中身の
を返す
8
9
10
11
node2 = node.children.last
pp node2
pp node2.type
12
pp node2.children
[出力結果]
1
(OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil))
19 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
の AST もまた `type` と
`children` を持っている
`1 + 2`
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
pp node.type
pp node.children
オブジェクトを渡すとブロックの中身の
を返す
8
9
10
node2 = node.children.last
pp node2
11
12
pp node2.type
pp node2.children
[出力結果]
1
2
:OPCALL
[(LIT@1:0-1:1 1), :+, (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)]
20 / 89
RubyVM::AbstractSyntaxTree のサンプル
[コード]
の AST もまた `type` と
`children` を持っている
このように AST は複数の AST から成り立
っている
`1 + 2`
1
2
3
node = RubyVM::AbstractSyntaxTree.parse("1 + 2")
# Proc
AST
# RubyVM::AbstractSyntaxTree.of(-> { 1 + 2 })
4
5
pp node
6
7
pp node.type
pp node.children
オブジェクトを渡すとブロックの中身の
を返す
8
9
10
node2 = node.children.last
pp node2
11
12
pp node2.type
pp node2.children
[出力結果]
1
2
:OPCALL
[(LIT@1:0-1:1 1), :+, (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)]
20 / 89
AST の対応表(一部) type コード AST `LIT` `1` `[:LIT, [1]]` `STR` `"string"` `[:STR, ["string"]]` `VCALL` `func` `[:VCALL, [:func]]` `CALL` `func.bar` `[:CALL, [[:VCALL, [:func]], :bar, nil]]` `QCALL` `func&.bar` `[:QCALL, [[:VCALL, [:func]], :bar, nil]]` 文字列リテラル メソッド呼び出し `.` 呼び出し `&.` 呼び出し `OPCALL` `1 + a` `[:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:VCALL, [:a]], nil]]]]` 演算子呼び出し `AND` `a && b` `[:AND, [[:LIT, [1]], [:VCALL, [:b]]]]` && 演算子 NOTE: 実データは `RubyVM::AST::Node` になるがわかりやすく配列で表記している 意味 数値やシンボルリテ ラルなど 21 / 89
マクロの変換プロセスの解説 22 / 89
マクロの変換プロセスの解説 簡単な例として `hoge.foo` を `hoge&.foo` に変換してみる 22 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo 23 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo このコードを AST に変換する 23 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AST::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) 24 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AST::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) このデータ構造のままだと扱いづらいの で一旦自前で配列に変換する 24 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AST::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:CALL, [[:VCALL, [:hoge]], :foo, nil]] 25 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AST::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:CALL, [[:VCALL, [:hoge]], :foo, nil]] AST 内の `CALL` という命令を これが `.` 演算子の命令 25 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AbstractSyntaxTree::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:QCALL, [[:VCALL, [:hoge]], :foo, nil]] `QCALL` という命令に置き換える `QCALL` が `&.` 演算子の命令 26 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo AST データ(RubyVM::AbstractSyntaxTree::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:QCALL, [[:VCALL, [:hoge]], :foo, nil]] この AST を Ruby のコードに変換する 27 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo 変換後の Ruby のコード 1 hoge&.foo AST データ(RubyVM::AbstractSyntaxTree::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:QCALL, [[:VCALL, [:hoge]], :foo, nil]] 28 / 89
AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo 変換後の Ruby のコード 1 hoge&.foo AST データ(RubyVM::AbstractSyntaxTree::Node) 1 2 (CALL@9:9-9:17 (VCALL@9:9-9:13 :hoge) :foo nil) AST (配列) 1 2 [:QCALL, [[:VCALL, [:hoge]], :foo, nil]] このようにして AST を書き換えることで別の Ruby のコードへと変更する事ができる 28 / 89
これがマクロだ!!! 29 / 89
このようにして AST の中身を書き換える事をマクロと定義する 30 / 89
このようにして AST の中身を書き換える事をマクロと定義する なのでマクロは AST から Ruby のコードへと変換する機能が必要になる 30 / 89
〜 AST から Ruby のコードに変換する 〜 Rensei - 錬成 - 31 / 89
Rensei - 錬成 - 32 / 89
Rensei - 錬成 - AST から Ruby のコードに復元するライブラリ https://github.com/osyo-manga/gem-rensei 32 / 89
Rensei - 錬成 - AST から Ruby のコードに復元するライブラリ https://github.com/osyo-manga/gem-rensei このライブラリを用いて `RubyVM::AST::Node` から Ruby のコードへと変換する からではなくて配列からも変換することができる 詳しくはこちらを(日本語) 【Ruby Advent Calendar 2020】Ruby の AST から Ruby のソースコードを復元しよう【1日目】 - Secret Garden(Instrumental) `RubyVM::AST::Node` https://secret-garden.hatenablog.com/entry/2020/12/01/093316 32 / 89
Rensei - 錬成 - AST から Ruby のコードに復元するライブラリ https://github.com/osyo-manga/gem-rensei このライブラリを用いて `RubyVM::AST::Node` から Ruby のコードへと変換する からではなくて配列からも変換することができる 詳しくはこちらを(日本語) 【Ruby Advent Calendar 2020】Ruby の AST から Ruby のソースコードを復元しよう【1日目】 - Secret Garden(Instrumental) `RubyVM::AST::Node` https://secret-garden.hatenablog.com/entry/2020/12/01/093316 AST が既に抽象化したデータになっているので元のコードを完全復元するわけではない ので注意 コメントの情報などは復元できない `()` などが追加されることもある 32 / 89
Rensei - 錬成 - AST から Ruby のコードに復元するライブラリ https://github.com/osyo-manga/gem-rensei このライブラリを用いて `RubyVM::AST::Node` から Ruby のコードへと変換する からではなくて配列からも変換することができる 詳しくはこちらを(日本語) 【Ruby Advent Calendar 2020】Ruby の AST から Ruby のソースコードを復元しよう【1日目】 - Secret Garden(Instrumental) `RubyVM::AST::Node` https://secret-garden.hatenablog.com/entry/2020/12/01/093316 AST が既に抽象化したデータになっているので元のコードを完全復元するわけではない ので注意 コメントの情報などは復元できない `()` などが追加されることもある 名前の由来は新しく Ruby のコードを生成する、という意味で付けた 32 / 89
Rensei の使い方 [コード] 1 2 3 require "rensei" 4 5 6 7 src = Rensei.unparse(node) puts src # => (((1 + 2) && hoge) || bar) 8 9 puts Rensei.unparse([:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:LIT, [2]], nil]]]]) # => (1 + 2) node = RubyVM::AbstractSyntaxTree.parse("1 + 2 && hoge || bar") 33 / 89
Rensei の使い方 [コード] 1 require "rensei" 2 3 4 5 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 && hoge || bar") src = Rensei.unparse(node) puts src 6 7 # => (((1 + 2) && hoge) || bar) 8 puts Rensei.unparse([:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:LIT, [2]], nil]]]]) 9 # => (1 + 2) Ruby のコードを AST に変更し 33 / 89
Rensei の使い方 [コード] 1 require "rensei" 2 3 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 && hoge || bar") 4 5 src = Rensei.unparse(node) puts src 6 7 # => (((1 + 2) && hoge) || bar) 8 puts Rensei.unparse([:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:LIT, [2]], nil]]]]) 9 # => (1 + 2) Ruby のコードを AST に変更し `RubyVM::AST::Node` から Ruby のコードへと復元し 33 / 89
Rensei の使い方 [コード] 1 require "rensei" 2 3 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 && hoge || bar") 4 src = Rensei.unparse(node) 5 6 7 puts src # => (((1 + 2) && hoge) || bar) 8 puts Rensei.unparse([:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:LIT, [2]], nil]]]]) 9 # => (1 + 2) Ruby のコードを AST に変更し `RubyVM::AST::Node` から Ruby のコードへと復元し 結果このような Ruby のコードが生成される `()` など元のコードにはない情報が付加されている 33 / 89
Rensei の使い方 [コード] 1 require "rensei" 2 3 node = RubyVM::AbstractSyntaxTree.parse("1 + 2 && hoge || bar") 4 5 src = Rensei.unparse(node) puts src 6 7 # => (((1 + 2) && hoge) || bar) 8 9 puts Rensei.unparse([:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:LIT, [2]], nil]]]]) # => (1 + 2) Ruby のコードを AST に変更し `RubyVM::AST::Node` から Ruby のコードへと復元し 結果このような Ruby のコードが生成される `()` など元のコードにはない情報が付加されている また配列の AST データからも復元することができる 33 / 89
Rensei を利用してマクロを実装した!!! 34 / 89
〜 Ruby で簡単にマクロを扱えるようにする 〜 Kenma - 研磨 - 35 / 89