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)和一些其它话题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容