[Elixir Macro #1]准备工作:defmacro
首先简化问题,先讨论无参数的 defmacro。
defmacro 最常见的用法,看长像是下面这样的:
defmacro m do quote do IO.puts "m" end end
最初学 macro 的时候,给我第一印象就是:defmacro 和 quote 是绑定的。
很显然,这是首先要纠正的东西,因为 defmacro 要求的输出是 AST,只不过恰好 quote 是用的比较多的生成 AST 的方法而已。
生成 AST 的方法还有很多,比如:
- 手写 AST
- 用 Macro.escape 来生成 AST
- 把多个 AST 组合成一个 AST
以下的例子,都是合法的 defmacro:
defmacro m do :ok end @opts %{key: :value} defmacro m do Macro.escape(@opts) end @a 3 defmacro m do [ if @a > 2 do quote do IO.puts "m1" end end, quote do IO.puts "m2" end ] end
Tips. 这里补充一下 quote 和 Macro.escape 的用法区别:
quote 是用来将「开发者手写的代码」转成 AST 的,Macro.escape 是用来将「变量」转成 AST 的,来个例子:
iex(1)> x = %{a: 1} %{a: 1} iex(2)> Macro.escape(x) {:%{}, [], [a: 1]} iex(3)> quote do %{a: 1} end {:%{}, [], [a: 1]} iex(4)> quote do x end {:x, [], Elixir}
接下来是宏的执行过程,一句话概括的话:宏返回 AST 并将返回的 AST 插入当前 AST 中。
这里可以用一个和宏等价的函数(只是实现相同的功能,并不是完全等价)来解释:
defmodule M do defmacro f1 do quote do IO.puts "f1" end end def f2 do quote do IO.puts "f2" end end end defmodule M2 do require M M.f1 M.f2 |> Code.eval_quoted([], __ENV__) end
以上便是 defmacro 的准备工作。
以上问题搞定之后,对于带参数的 macro,其实已经没什么问题了,因为「参数」同样可以一句话概括:参数会在被转成 AST 之后传给宏。
这个同样可以用宏的等价函数来解释:
defmodule M do defmacro f1(x) do quote do IO.inspect unquote(x) end end def f2(x) do quote do IO.inspect unquote(x) end end end defmodule M2 do require M %{key: :value} |> M.f1() quote do %{key: :value} end |> M.f2() |> Code.eval_quoted([], __ENV__) end
所以总结一下,想说明的主要有以下几点:
- quote 本身是一个宏,返回值是 AST,和 defmacro 没有绑定关系
- 宏的参数是 AST
- 宏的输出是 AST
- 宏输出的 AST 会被插入到当前的 AST 中