查看源代码 引用与解引用
本指南旨在介绍 Elixir 中可用的元编程技术。用自身数据结构表示 Elixir 程序的能力是元编程的核心。本章首先探讨这些结构以及相关的 quote/2
和 unquote/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 的函数。
在本介绍中,我们为最终编写第一个宏奠定了基础。您可以在下一指南中查看它。