RubyKaigi Takeout 2021 で Ruby のマクロについて話します

先日 RubyKaigi Takeout 2021 のスピーカーとスケジュールが発表されました。
わたしも 3日目の朝一に Use Macro all the time ~ マクロを使いまくろ ~というタイトルで登壇させていただきます。
内容はタイトルにも書かれている通りマクロの話をします。
マクロと言ってもいわゆる C言語のようなプリプロセスマクロではなくて『AST レベルで Ruby の構文を別の構文へと変換すること』を Ruby のマクロと定義し、Ruby でどのようにマクロを表現するのか、みたいな話をする予定です。
まあもっとさっくりう言うと『Ruby でマクロを実装してみたらこうなりました』みたいな感じですかねえ。
例えばマクロを使用することで、

hoge.foo.bar

のようなコードを

hoge&.foo&.bar

のようにぼっち演算子呼び出しとして変換する事ができたり、

CONST_HOGE = [1, 2, 3]

というような定数定義しているコードを

CONST_HOGE = [1, 2, 3].freeze

のように暗黙的に freeze したり、

![a, b, c]

というようなコードを

{ a: a, b: b, c: c }

みたいな Hash に展開するようなことを実現することができます。
Ruby のマクロは以下のような過程を経て Ruby のコードから別の Ruby のコードへと変換しています。

開発中のコードをちょっとだけお見せすると Ruby のマクロを使うことによって以下のようなことを実現することができます。

# ※開発中のコードなので当日までに変わるかもね!
using Macro::Refine::Source


# 特定の構文を変換するマクロを定義する
module MyMacro
  using Macro::Macroable

  # cat! というメソッド呼び出しを任意のコードに変換する
  def cat!(num = ast { 1 })
    ast { "にゃーん" * $num }
  end
  macro_function :cat!

  # 範囲リテラルを freeze するコードに変換する
  def freezing(node, parent)
    ast { $node.freeze }
  end
  macro_node :DOT2, :freezing
  macro_node :DOT3, :freezing

  # 代入式を freeze するコードに変換する
  def assign_freezing(node, name:, value:)
    ast { $name = $value.freeze }
  end
  macro_pattern pat { $name = $value }, :assign_freezing
end


# 変換したい Ruby のコードを Proc で定義
body = proc {
  use_macro! MyMacro

  puts cat!
  puts cat!(3)

  value = [2, 3, 5, 7, 11, 13, 17].grep((0..10))

  HOGE = "にゃーん" + "にゃーん"
}

# マクロを適用させた AST に変換する
result = Macro.compile_of(body)

# 変換させた AST から Ruby のソースコードを取得する
pp result.source

# 以下のようなコードに変換される
__END__

# cat!(n) が "にゃーん" * n に変換される
puts "にゃーん" * 1
puts "にゃーん" * 3

# (0..10) が (0..10).freeze に変換される
# value = hoge が value = hoge.freeze に変換される
value = [2, 3, 5, 7, 11, 13, 17].grep((0..10).freeze).freeze

HOGE = ("にゃーん" + "にゃーん").freeze

上記のような書き方で Ruby のコードを AST レベルで別のコードへと変換するような実装を書いています。
ちなみに実装はすべて Ruby で行っているので MRI 側で特に何か機能を追加したりとかはしていません。
この登壇でメタプロの向こう側ってやつを見せてやりますよ(誇張。
と、言うことでこういう話が好きな方は当日までお楽しみに! ちなみに去年を除くとこれが RubyKaigi 初参加になります。
RubyKaigi 初参加で初登壇の実績を解除したぜ…。