[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

所以总结一下,想说明的主要有以下几点:

  1. quote 本身是一个宏,返回值是 AST,和 defmacro 没有绑定关系
  2. 宏的参数是 AST
  3. 宏的输出是 AST
  4. 宏输出的 AST 会被插入到当前的 AST 中