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

1.2K Views

December 23, 21

スライド概要

https://rubykaigi.org/2021-takeout/presentations/pink_bangbi.html

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Use Macro all the time ~ マクロを使いまくろ ~ RubyKaigi Takeout 2021

2.

やあ、みんな おはよう こんにちわ こんばんわ

3.

みんな Ruby を使ってい る???

4.

Ruby を使っていると…

5.

1 CONST_VALUE = [1, 2, 3] こういう定数定義を

6.

1 CONST_VALUE = [1, 2, 3] こういう定数定義を 1 CONST_VALUE = [1, 2, 3].freeze 暗黙的に `freeze` させたり

7.

1 puts config.hoge_flag 2 puts config.foo_flag みたいなデバッグ出力を

8.

1 puts config.hoge_flag 2 puts config.foo_flag みたいなデバッグ出力を 1 # output: 2 "config.hoge_flag # => true" 3 "config.foo_flag # => false" みたいに出力内容と出力結果を一緒に出力させたり

9.

1 ![a, b, c] こういうコードを

10.

1 ![a, b, c] こういうコードを 1 { a: a, b: b, c: c } みたいに Hash で展開させたりとか

11.

やりたくなりますよね!!

12.

それマクロでできるよ!!!!

13.

自己紹介 名前: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

14.

今日話すこと 11 / 89

15.

今日話すこと Ruby でマクロを実装した話 11 / 89

16.

アジェンダ Ruby のマクロとは AST とは マクロの変換プロセスの解説 Rensei - 錬成 - AST から Ruby のコードを生成するライブラリ Kenma - 研磨 - 任意の AST を別の AST に変換するライブラリ マクロの使用例 これからの課題 12 / 89

17.

マクロとは? 13 / 89

18.

マクロとは 14 / 89

19.

マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる 14 / 89

20.

マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる この登壇では『Ruby の AST を別の AST に変換すること』を『Ruby のマクロ』と定義 14 / 89

21.

マクロとは 世の中にはいろいろなマクロがある C言語マクロ、LISP マクロ、Rust マクロ、エクセルのマクロ etc… マクロと言ってもそれぞれ意味が異なる この登壇では『Ruby の AST を別の AST に変換すること』を『Ruby のマクロ』と定義 この『マクロ』を使用すると Ruby のコードを構文レベルで変更する事ができる 例えば `hoge.foo` を `hoge&.foo` に変更したり 理論上は valid な Ruby のコードであればどんなコードにでも変換できる 14 / 89

22.

そもそも AST って? 15 / 89

23.

AST とは 16 / 89

24.

AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 16 / 89

25.

AST とは AST とは抽象構文木(Abstract Syntax Tree)の略 今回は `RubyVM::AbstractSyntaxTree` を使用する で使用される実データは `RubyVM::AbstractSyntaxTree::Node` だが、 このスライドでは一部配列形式で記述している 以下 `RubyVM::AST::Node` と略 `RubyVM::AbstractSyntaxTree` 16 / 89

26.

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

27.

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

28.
[beta]
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

29.
[beta]
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

30.
[beta]
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

31.
[beta]
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

32.
[beta]
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

33.
[beta]
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

34.
[beta]
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

35.
[beta]
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

36.
[beta]
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

37.

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

38.

マクロの変換プロセスの解説 22 / 89

39.

マクロの変換プロセスの解説 簡単な例として `hoge.foo` を `hoge&.foo` に変換してみる 22 / 89

40.

AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo 23 / 89

41.

AST の変換プロセス 変換前の Ruby のコード 1 hoge.foo このコードを AST に変換する 23 / 89

42.

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

43.

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

44.

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

45.

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

46.

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

47.

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

48.

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

49.

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

50.

これがマクロだ!!! 29 / 89

51.

このようにして AST の中身を書き換える事をマクロと定義する 30 / 89

52.

このようにして AST の中身を書き換える事をマクロと定義する なのでマクロは AST から Ruby のコードへと変換する機能が必要になる 30 / 89

53.

〜 AST から Ruby のコードに変換する 〜 Rensei - 錬成 - 31 / 89

55.

Rensei - 錬成 - AST から Ruby のコードに復元するライブラリ https://github.com/osyo-manga/gem-rensei 32 / 89

56.

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

57.

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

58.

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

59.

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

60.

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

61.

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

62.

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

63.

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

64.

Rensei を利用してマクロを実装した!!! 34 / 89

65.

〜 Ruby で簡単にマクロを扱えるようにする 〜 Kenma - 研磨 - 35 / 89