5. Macros

5 宏(Macros)


一段Elixir程序可以展现为一组数据结构。本章将说明这些结构(看起来)是怎样的,以及如何使用它们来创建你自己的宏。

5.1 Elixir程序的建构block(building block)


Elixir程序的建构block是一个包含三个元素的元组。例如,名为 sum(1,2,3) 的函数在 Elixir 中可以被展现为这样:

{ :sum, [], [1, 2, 3] }

你可以使用 quote 这个宏来获取任意表达式的展现方式:

iex> quote do: sum(1, 2, 3)
{ :sum, [], [1, 2, 3] }

操作符同样能被展现为这样的元组:

iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}

甚至一个元组也可以表现为对 {} 的一次调用:

iex> quote do: { 1, 2, 3 }
{ :{}, [], [1, 2, 3] }

变量也能用类似元组表现,只是最后一个元组不再是一个list,而是atom原子:

iex> quote do: x
{ :x, [], Elixir }

当我们引用(译注:quoting,指用 quote 宏来展现一个表达式)更复杂的表达式时,可以看到其展现方式是元组组合而成,其中的元组彼此嵌套为相似的树,其中的每个节点都是元组。

iex> quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}

通常,上述的每个节点(即元组)会遵循下列格式:

{ tuple | atom, list, list | atom }

元组的第一个元素是是一个atom原子或类似的展现元组

元组的第二个元素是一个元数据list,它将存放类似节点行号的元信息

元组的第三个元素可能是一个参数列表或者一个atom原子,如果是后者,表示这个元组是展现一个变量

除了上面定义的节点,有5种Elixir字面量会在引用(解释同上)时返回其本身(不是元组)。他们是:

:sum         #=> Atoms
1.0          #=> Numbers
[1,2]        #=> Lists
"binaries"   #=> Strings
{key, value} #=> Tuples with two elements

掌握了这些基本概念后,我们就可以准备定义自己的宏了。


5.2 定义我们自己的宏


可以使用 defmacro 定义一个宏。例如,通过下面少量几行代码,我就可以定义一个名叫 unless 的宏,让它起到和 if 相反的效果:

defmodule MyMacro do
  defmacro unless(clause, options) do
    quote do: if(!unquote(clause), unquote(options))
  end
end

类似 if , unless 需要两个参数—— clause 和 options :

require MyMacro
MyMacro.unless var, do: IO.puts "false"

这样,既然 unless 是一个宏,它的参数就不应当在(unless)被调用时求值,而是应该直接传入字面量。比如,如果有一个这样的调用

unless 2 + 2 == 5, do: call_function()

我们的 unless 宏将会接受到下面的信息:

unless({:==, [], [{:+, [], [2, 2]}, 5]}, { :call_function, [], [] })

那么 unless 宏就要调用 quote 来返回一个 if 语句的结构树,这意味着我们将 unless 转换为 if!

引用表达式的时候一个常见的错误是开发者常常会忘记 unquote 那个表达式。为了理解 unquote 所做的事情,让我们简单的去除它看看结果:

defmacro unless(clause, options) do
  quote do: if(!clause, options)
end

当我们调用unless 2 + 2 == 5, do: call_function(), 我们的unless将返回字面量

if(!clause, options)

由于clause和options这两个变量没有定义在当前上下文中,执行就会失败。而如果我们把unquote添加回来:

defmacro unless(clause, options) do
  quote do: if(!unquote(clause), unquote(options))
end

unless 将会返回:

if(!(2 + 2 == 5), do: call_function())

换句话说,unquote 是一个将表达式注入到被引用的解析树的机制,同时也是元编程的核心工具。Elixir同时还提供 unquote_splicing 来允许我们一次注入多个表达式

我们可以定义我们需要的任何宏——甚至可以覆盖掉Elixir内建的宏。例如,你可以重新定义 case , receive , + ... 等等。但是 Elixir 有一些特殊形式不能被覆盖,在 Kernel.SpecialForms 中有这些形式的完整列表。

5.3 宏的安全性


Elixir宏会被延迟解析,这将保证在展开宏时,定义在quote中的变量不会与上下文中的变量定义冲突,例如:

defmodule Hygiene do
  defmacro no_interference do
    quote do: a = 1
  end
end

defmodule HygieneTest do def go do require Hygiene a = 13 Hygiene.no_interference a end end HygieneTest.go # => 13

在上述例子中,即使在宏中注入 a = 1 ,那也不会影响到定义在 go 函数中的a变量。在某些场景中,宏需要显式的影响上下文(的变量),我们可以使用 var!:

defmodule Hygiene do
  defmacro interference do
    quote do: var!(a) = 1
  end
end
defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.interference
    a
  end
end
HygieneTest.go
# => 1

安全的变量仅仅由于Elixir在相应上下文中标注了变量而正常工作。例如,变量 x 定义在模块的第三行,展开以后就是这样:

{ :x, [line: 3], nil }

而一个被引用的变量(代码中未定义)展开以后就会是这样:

defmodule Sample do
  def quoted do
    quote do: x
  end
end
Sample.quoted #=> { :x, [line: 3], Sample }

注意:在引用变量的展现方式里的第三个元素是一个原子 Sample ,而不是 nil ,这标记了这个变量来自 Sample 这个module。这样,Elixir就能根据这些信息正确处理这两个来自不同上下文的变量。

Elixir为imports和aliases提供了相似的机制,以确保宏将与其所在的特定源码行为一致而不是与目标模块冲突。

5.4 私有宏


Elixir使用 defmacrop 支持私有宏。这些宏将仅能在所定义的模块内部被使用,就像私有函数,只不过它是在编译时工作的。一个常见的关于私有宏的例子是定义在同一个模块内部被频繁使用的 guard :

defmodule MyMacros do
  defmacrop is_even?(x) do
    quote do
      rem(unquote(x), 2) == 0
    end
  end
  def add_even(a, b) when is_even?(a) and is_even?(b) do
    a + b
  end
end

很重要的一点是:宏必须在使用之前定义。如果没有在调用前定义宏,那么我们将收到一个运行时错误,因为此时宏无法被展开并转换为函数调用:

defmodule MyMacros do
  def four, do: two + two
  defmacrop two, do: 2
end
MyMacros.four #=> ** (UndefinedFunctionError) undefined function: two/0


5.5 代码执行


在结束关于宏的讨论之前,我们将简短的论述代码是如何在Elixir中被执行的。在Elixir中,代码的完整执行涉及两个步骤:

1) 代码中的所有宏将被递归的展开;

2) 被展开的代码将被编译为Erlang字节码并被执行

理解这些非常重要,因为这会影响我们如何看待我们的代码结构。看看如下的代码:

defmodule Sample do
  case System.get_env("FULL") do
    "true" ->
      def full?(), do: true
    _ ->
      def full?(), do: false
  end
end

上述代码将定义一个名为 full? 的函数,它将根据编译时的环境变量 FULL 的值返回 true 或者 false。为了执行这段代码,Elixir将首先展开所有的宏。而因为 defmodule 和 def 本身也是宏,代码将被展开为类似下面的样子:

:elixir_module.store Sample, fn ->
  case System.get_env("FULL") do
    "true" ->
      :elixir_def.store(Foo, :def, :full?, [], true)
    _ ->
      :elixir_def.store(Foo, :def, :full?, [], false)
end


接着,代码将被执行,定义一个名为 Foo的模块,并在这个模块内部存放一个关联的函数,这个函数基于环境变量 FULL 的值。达成这些需要使用 elixir_module 和 elixir_def 函数,这两个函数都是来自Elixir内部模块,本身使用erlang编写。

这个例子中我们可以学到两点:

1) 宏总是会被展开的,无论它所在 case 的分支是否会被执行到;

2) 我们不能紧接着一个函数或者宏的定义之后来调用它,例如如下代码:

defmodule Sample do
  def full?, do: true
  IO.puts full?
end

这段代码将会失败,因为它会被转换成这样:

:elixir_module.store Sample, fn ->
  :elixir_def.store(Foo, :def, :full?, [], true)
  IO.puts full?
end

此时,模块正在被定义,(因而)还没有一个名为 full? 的函数被定义在模块中,这样, IO.puts full? 调用就会遇到编译失败。

5.6 避免使用宏Don't write macros


考虑到宏是一个很强大的编程结构,在这个领域的第一条原则是——避免使用宏。相比于普通的Elixir函数,宏的编写是比较难的,在不必要的时候使用宏被认为是一个不好的风格(bad style)。Elixir已经提供了很多优雅的机制帮助你日常的编码工作,宏应当被作为最后手段。

通过上述课程,我们结束了对宏的介绍。接下来,让我们进入下一章,讨论代码文档、非完整应用(partial application)和一些其它话题。

你可能感兴趣的:(5. Macros)