前言

尽管Elixir尽力为宏提供一个安全的环境,但使用宏编写干净的代码的主要责任落在了开发人员身上。宏比普通的Elixir函数更难编写,在不需要的时候使用它们被认为是不好的风格。因此,负责任地编写宏。

Elixir已经提供了使用其数据结构和功能以简单易读的方式编写日常代码的机制。宏只能作为最后的手段使用。请记住,显式比隐式更好清晰的代码比简洁的代码更好。

我们的第一个宏

Elixir中的宏通过定义defmacro/2

在本章中,我们将使用文件而不是在IEx中运行代码示例。这是因为代码示例将跨越多行代码并在IEx中全部输入它们可能会适得其反。您应该能够通过将代码示例保存到macros.exs文件中并使用elixir macros.exs或运行来运行代码示例iex macros.exs

为了更好地理解宏是如何工作的,让我们创建一个我们要实现的新模块unless,它与if宏和函数相反:

defmodule Unless do
  def fun_unless(clause, do: expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

该函数接收参数并将它们传递给它们if。但是,正如我们在前一章中所了解的那样,宏将收到带引号的表达式,将它们插入到引用中,最后返回另一个带引号的表达式。

我们从iex上面的模块开始:

$ iex macros.exs

并利用这些定义:

iex> require Unless
iex> Unless.macro_unless true, do: IO.puts "this should never be printed"
nil
iex> Unless.fun_unless true, do: IO.puts "this should never be printed"
"this should never be printed"
nil

请注意,在我们的宏实现中,虽然它是在我们的函数实现中打印的,但并未打印该句子。这是因为函数调用的参数在调用函数之前被评估。但是,宏不会评估它们的参数。相反,他们收到参数作为引用的表达式,然后转换为其他引用的表达式。在这种情况下,我们重写了我们的unless宏观,成为if幕后。

换句话说,当被调用为:

Unless.macro_unless true, do: IO.puts "this should never be printed"

我们的macro_unless宏收到以下内容:

macro_unless(true, [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["this should never be printed"]}])

然后它返回一个引用的表达式如下:

{:if, [],
 [{:!, [], [true]},
  [do: {{:., [],
     [{:__aliases__,
       [], [:IO]},
      :puts]}, [], ["this should never be printed"]}]]}

实际上,我们可以通过使用Macro.expand_once/2以下内容来验证是否属实:

iex> expr = quote do: Unless.macro_unless(true, do: IO.puts "this should never be printed")
iex> res  = Macro.expand_once(expr, __ENV__)
iex> IO.puts Macro.to_string(res)
if(!true) do
  IO.puts("this should never be printed")
end
:ok

Macro.expand_once/2收到引用的表达式并根据当前环境进行扩展。在这种情况下,它展开/调用Unless.macro_unless/2宏并返回结果。然后我们继续将返回的引用表达式转换为一个字符串并打印出来(我们将__ENV__在本章后面讨论)。

这就是宏的全部内容。它们是关于接受引用的表达式并将它们转化为其他东西。实际上,unless/2Elixir是作为一个宏实现的:

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

结构,如unless/2defmacro/2def/2defprotocol/2,和其他许多人在本入门指南使用的纯药剂来实现,通常为宏。这意味着用于构建语言的构造可以被开发人员用来将语言扩展到他们正在处理的领域。

我们可以定义我们想要的任何函数和宏,包括覆盖Elixir提供的内置定义的函数和宏。唯一的例外是Elixir在Elixir中没有实施的特殊表格,因此不能被覆盖,特殊表格的完整列表可在Kernel.SpecialForms

宏卫生

Elixir宏已经解析晚了。这可以确保在引用内定义的变量不会与在展开该宏的上下文中定义的变量冲突。例如:

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,它也不会影响函数a定义的变量go。如果一个宏想明确地影响上下文,它可以使用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模块的第3行定义的变量将表示为:

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

但是,引用的变量表示为:

defmodule Sample do
  def quoted do
    quote do: x
  end
end

Sample.quoted #=> {:x, [line: 3], Sample}

请注意,引用变量中的第三个元素是原子Sample,而不是nil将变量标记为来自Sample模块的原子。因此,Elixir认为这两个变量来自不同的背景,并据此处理。

Elixir也提供了类似的进口和别名机制。这保证了宏将按照其源模块的指定行为,而不是与扩展宏的目标模块相冲突。卫生可以在特定情况下使用宏等绕过var!/2alias!/2,虽然必须使用这些时,他们直接改变用户环境中要小心。

有时可能会动态创建变量名称。在这种情况下,Macro.var/2可以用来定义新的变量:

defmodule Sample do
  defmacro initialize_to_char_count(variables) do
    Enum.map variables, fn(name) ->
      var = Macro.var(name, nil)
      length = name |> Atom.to_string |> String.length
      quote do
        unquote(var) = unquote(length)
      end
    end
  end

  def run do
    initialize_to_char_count [:red, :green, :yellow]
    [red, green, yellow]
  end
end

> Sample.run #=> [3, 5, 6]

注意第二个参数Macro.var/2。这是正在使用的上下文,并将确定下一节所述的卫生。

环境

当调用Macro.expand_once/2本章前面,我们使用了特殊形式__ENV__

__ENV__返回Macro.Env包含有关编译环境的有用信息的结构实例,包括当前模块,文件和行,当前范围中定义的所有变量以及导入,需求等等:

iex> __ENV__.module
nil
iex> __ENV__.file
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
nil
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]

Macro模块中的许多功能都需要一个环境。你可以阅读更多有关这些功能的文档的Macro模块,并了解更多有关在编译环境文档的Macro.Env

私有宏

Elixir还支持通过私有宏defmacrop。作为私有函数,这些宏只能在定义它们的模块内部使用,并且只能在编译时使用。

在使用之前定义一个宏是很重要的。在调用宏之前未能定义宏会在运行时产生错误,因为宏不会被扩展并且会被转换为函数调用:

iex> defmodule Sample do
...>  def four, do: two + two
...>  defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined

负责任地编写宏

宏是一个强大的构造,Elixir提供了许多机制来确保它们的使用是负责任的。

  • 宏是卫生的:默认情况下,宏内定义的变量不会影响用户代码。此外,宏上下文中可用的函数调用和别名不会泄漏到用户上下文中。
  • 宏是词法的:全局注入代码或宏是不可能的。为了使用宏,您需要显式地require或者import定义宏的模块。
  • 宏是明确的:不显式调用宏就不可能运行宏。例如,一些语言允许开发人员在幕后完全重写函数,通常通过解析变换或通过一些反射机制。在Elixir中,必须在编译期间在调用者中显式调用宏。
  • 宏的语言很清楚:许多语言都提供了quote和的语法快捷键unquote。在Elixir中,为了明确界定宏定义及其引用表达式的边界,我们倾向于明确阐述它们。

即使有了这样的保证,开发人员在负责任编写宏时也扮演着重要的角色。如果您确信需要使用宏,请记住宏不是您的API。保持简短的宏定义,包括他们的引用内容。例如,而不是像这样写一个宏:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      do_this(unquote(a))
      ...
      do_that(unquote(b))
      ...
      and_that(unquote(c))
    end
  end
end

写:

defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      # Keep what you need to do here to a minimum
      # and move everything else to a function
      MyModule.do_this_that_and_that(unquote(a), unquote(b), unquote(c))
    end
  end

  def do_this_that_and_that(a, b, c) do
    do_this(a)
    ...
    do_that(b)
    ...
    and_that(c)
  end
end

这使得您的代码更清晰,更易于测试和维护,因为您可以do_this_that_and_that/3直接调用和测试。它还可以帮助您为不想依赖宏的开发人员设计实际的API。

通过这些课程,我们完成了对宏的介绍。下一章是关于DSL的简要讨论,它展示了我们如何混合宏和模块属性来注释和扩展模块和功能。