查看源代码 引用与解引用

本指南旨在介绍 Elixir 中可用的元编程技术。用自身数据结构表示 Elixir 程序的能力是元编程的核心。本章首先探讨这些结构以及相关的 quote/2unquote/1 结构,以便我们可以在下一指南中查看宏,并最终构建自己的领域特定语言。

引用

Elixir 程序的基本组成部分是一个包含三个元素的元组。例如,函数调用 sum(1, 2, 3) 在内部表示为

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

可以使用 quote/2 宏获取任何表达式的表示形式

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}
{:%{}, [], [{1, 2}]}

变量使用这样的三元组表示,不同之处在于最后一个元素是原子,而不是列表

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

引用更复杂的表达式时,我们可以看到代码是用这样的元组表示的,这些元组通常相互嵌套,形成类似树的结构。许多语言将这种表示称为 抽象语法树 (AST)。Elixir 称它们为引用表达式

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

有时,在处理引用表达式时,获取文本代码表示可能很有用。这可以使用 Macro.to_string/1 来完成

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

一般来说,上面的元组结构遵循以下格式

{atom | tuple, list, list | atom}
  • 第一个元素是原子或另一个相同表示的元组;
  • 第二个元素是包含元数据的关键字列表,例如数字和上下文;
  • 第三个元素是函数调用的参数列表或原子。当此元素是原子时,表示元组代表变量。

除了上面定义的元组之外,还有五个 Elixir 字面量,它们在引用时返回自身(而不是元组)。它们是

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

大多数 Elixir 代码都有一个直接的翻译到其底层的引用表达式。我们建议您尝试不同的代码示例并查看结果。例如,String.upcase("foo") 展开到什么?我们还了解到 if(true, do: :this, else: :that)if true do :this else :that end 相同。这个断言在引用表达式中是如何成立的?

解引用

引用是关于检索特定代码块的内部表示。但是,有时可能需要在要检索的表示中注入其他特定代码块。

例如,假设您有一个名为 number 的变量,它包含您想要注入引用表达式中的数字。

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

这不是我们想要的,因为 number 变量的值没有被注入,并且 number 在表达式中被引用了。为了注入 number 变量的,必须在引用表示中使用 unquote/1

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

unquote/1 甚至可以用来注入函数名

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

在某些情况下,可能需要在列表中注入多个值。例如,假设您有一个包含 [1, 2, 6] 的列表,并且我们想要将 [3, 4, 5] 注入其中。使用 unquote/1 不会产生预期的结果

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 就派上用场了

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

解引用在处理宏时非常有用。在编写宏时,开发人员能够接收代码块并将它们注入其他代码块中,这可以用于转换代码或编写在编译期间生成代码的代码。

转义

正如我们在本章开头所看到的,只有某些值在 Elixir 中是有效的引用表达式。例如,映射不是有效的引用表达式。包含四个元素的元组也不是。但是,这些值可以用引用表达式来表示

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

在某些情况下,您可能需要将这些注入引用表达式。为此,我们需要首先使用 Macro.escape/1 将这些值转义为引用表达式

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

宏接收引用表达式,并且必须返回引用表达式。但是,有时在宏执行期间,您可能需要处理值,并且需要区分值和引用表达式。

换句话说,区分常规 Elixir 值(如列表、映射、进程、引用等)和引用表达式很重要。某些值,如整数、原子和字符串,它们的引用表达式等于值本身。其他值,如映射,需要显式转换。最后,诸如函数和引用之类的值根本无法转换为引用表达式。

在处理宏和生成代码的代码时,请查看 Macro 模块的文档,该模块包含许多用于处理 Elixir AST 的函数。

在本介绍中,我们为最终编写第一个宏奠定了基础。您可以在下一指南中查看它。