Elixir中的元编程-Quote 和 Unquote

  1. Quoting
  2. Unquoting
  3. Escaping

一个Elixir程序可以由Elixir自己的数据结构表示。在本章里,我们将学习那些数据结构看起来是什么样的,以及如何组成它们。在本章中学习的概念是宏的构建块,我们将在下一章中更深入地研究它。

Quoting

一个Elixir程序的构建块上一个三元素的元组。例如,一个函数调用 sum(1, 2, 3) 被内部表示为:

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

你通过使用 quote 宏得到任何表达式的表示:

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

第一个元素是函数名,第二个元素是一个包含元数据的关键字列表,第三个元素是参数列表。

运算符也可以表示为这样的元组:

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

甚至一个映射被表示为对 %{} 的调用:

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

变量也可以用这样的三元素的元组来表示,其最后一个元素不是一个列表而是一个原子:

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

当引用更负责的表达式,我们可以看到代码用这样的元组来表示,这样的元组经常彼此嵌套为一个像一颗树的结构。许多语言叫这样的表示为抽象语法树(AST)。Elixir叫它们为被引用的表达式:

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

有时使用引用表达式时,返回代码的文本表示可能是有用的。这可以用 Macro.to_string/1 :

1
2
iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"

总的来说,以上元组按以下格式构造:

1
{atom | tuple, list, list | atom}
  • 第一个元素是一个原子或是相同表示格式的另一个元组。
  • 第二个元素是一个包含元数据,比如数字和上下文,的关键字列表。
  • 第三个元素是一个函数调用的参数列表或者一个原子。当这个元素是一个原子,则意味着这个元组表示一个变量。

除了上述定义的元组,有五个Elixir字面量,当被引用的时候,返回的是它们自己(而不是一个元组)。它们是:

1
2
3
4
5
:sum #=> 原子
1.0 #=> 数字
[1, 2] #=> 列表
"strings" #=> 字符串
{key, value} #=> 两个元素的元组

大多数Elixir代码有一个它的底层引用表达式的直接翻译。我们建议你尝试不同的代码样例然后看看结果是怎么样的。例如,String.upcase(“foo”) 展开后是什么样子?我们也学到了 if(true, do: :this, else: :that) 和 if true do :this else :that end 是一样的。这个肯定是如何用引用表达式支持的?

Unquoting

quote是关于获取一些特指代码块的内部表示。但是,有时候可能必须注入一些其他特指代码块到我们想要获取的表示里。

例如,你有一个包含数字的变量number,你想要把它注入到一个引用表达式里。

1
2
3
iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"

这不是我们想要的,因为number变量的值没有被注入并且number已经被引入表达式。为了注入number变量的值,unquote必须被使用在被引用的描述里:

1
2
3
iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"

unquote甚至可以被用于注入函数名:

1
2
3
iex> fun = :hello
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"

在一些场景里,可能必须注入许多值在一个列表里。例如,假设你有一个列表 [1, 2, 6] 并且你想把 [3, 4, 5] 注入进去。使用 unquote 不会长生想要的结果:

1
2
3
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"

这个时候就要使用 unquote_splicing :

1
2
3
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"

当使用宏的时候,Unquoting 非常有用。当写宏的时候,开发者能够接收代码块并且注入它们到其他代码块里,这能被用来转换代码或者写代码,在编译期间生成代码。

Escaping

正如我们在本章开始的时候所见,在Elixir里只有一些值是有效引用表达式。例如,一个映射不是一个有效引用表达式。有四个元素的元组也不是。但是,这样的值可以被表达为一个引用表达式:

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

在一些场景下,你可能需要注入这样的值到引用表达式里。为了这么做,我们需要首先用 Macro.escape/1 帮主来转义那些值为引用表达式:

1
2
3
iex> map = %{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}

宏接收引用表达式并且必须返回引用表达式。然后,有些时候,在宏执行期间,你可能需要使用值并且使得值和将被需求的引用表达式之间有差别。

换句话说,使得一个普通Elixir值(像一个列表、一个映射、一个进程、一个引用等等)和一个引用表达式之间有差别是很重要的。一些值,比如,整数、原子和字符串,有一个引用表达式等于值的本身。其他值,像映射,需要被明确地转换。最后,像函数和引用这样的值完全不能被转换为引用表达式。

Kernel.SpecialForms 模块里,你可以阅读到跟多关于 quote 和 unquote 的内容。Macro.escape/1 和其他与引用表达式相关的函数的文档可以在 Macro 模块里找到。

在这篇介绍文章里,我们已经为最终写我们的第一个宏奠定了基础,所以让我们开始下一章。

原文链接: http://elixir-lang.org/getting-started/meta/quote-and-unquote.html