动态生成的宏的双重取消引用

Double unquoting for dynamically generated macros

鉴于以下情况:

  for fn_name <- [:foo, :bar, :baz] do
    defmacro unquote(fn_name)(do: inner) do
      fn_name = unquote(fn_name) # <--- Why?
      quote do
        IO.puts "#{unquote(fn_name)} called"
        unquote(inner)
      end
    end
  end

fn_name = unquote(fn_name)的原因是什么?如果我省略这一行,那就是编译错误。 "double" 取消引用的原因是什么?

这是因为卫生

Elixir 有宏卫生 的概念。卫生意味着您在宏中定义的变量、导入和别名不会泄漏到调用者自己的定义中。

for fn_name <- [:foo, :bar, :baz] do
    defmacro unquote(fn_name)(do: inner) do
      fn_name = unquote(fn_name) # <-- This is macro's context
      quote do
        IO.puts "#{unquote(fn_name)} called" # <-- This is caller's context
        unquote(inner)
      end
    end
  end

您应该阅读 Chris McCord 的 Metaprogramming Elixir 书Hygiene Protects the Caller's Context

让我们稍微简化一下示例:

for fn_name <- [:foo, :bar, :baz] do
  defmacro unquote(fn_name)(do: inner) do
    fn_name = unquote(fn_name) # <--- Why?
    quote do
      {unquote(fn_name), unquote(inner)}
    end
  end
end

在上面的例子中,因为 quote 返回一个包含两个不带引号的元素的元组,所以它等价于:

for fn_name <- [:foo, :bar, :baz] do
  defmacro unquote(fn_name)(do: inner) do
    fn_name = unquote(fn_name) # <--- Why?
    {fn_name, inner}
  end
end

现在更容易理解如果您之前不 unquote(fn_name) 会发生什么:变量 fn_name 根本不会存在于宏定义中。请记住,所有 def(def、defp、defmacro 等)都会启动一个新的变量作用域,因此如果要在内部使用 fn_name,则需要以某种方式定义它。

我们在此代码中看到的另一个 属性 是 Elixir 在看到 quote 时将停止取消引用。所以在上面的引用中, unquote 不会在定义宏时取消引用,而是在执行宏时取消引用,这也解释了为什么需要在宏内部定义变量。