引用和取消引用

本指南旨在介绍Elixir中提供的元编程技术。通过自己的数据结构表示Elixir程序的能力是元编程的核心。本章将通过探索这些结构和相关的启动quoteunquote构造,所以我们可以看看宏在下一章,最终建立我们自己的领域特定语言。

Elixir指南也以EPUB格式提供:

引用

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

{: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}
{:%{}, [], [{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。引用的表达式如何保持这种肯定?

不引用

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

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

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

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

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

unquote 甚至可以用来注入函数名称:

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

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

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

那时候unquote_splicing变得很方便:

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值(如列表,地图,进程,引用等)和引用表达式是非常重要的。某些值(如整数,原子和字符串)的引用表达式等于该值本身。其他值,如地图,需要明确转换。最后,函数和引用之类的值根本无法转换为带引号的表达式。

你可以阅读更多关于quoteunquoteKernel.SpecialForms模块Macro.escape/1与引用表达式相关的文档和其他功能可以在Macro模块中找到。

在这篇介绍中,我们已经奠定了基础,最终编写我们的第一个宏,所以让我们转到下一章。