引用和取消引用
本指南旨在介绍Elixir中提供的元编程技术。通过自己的数据结构表示Elixir程序的能力是元编程的核心。本章将通过探索这些结构和相关的启动quote
和unquote
构造,所以我们可以看看宏在下一章,最终建立我们自己的领域特定语言。
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值(如列表,地图,进程,引用等)和引用表达式是非常重要的。某些值(如整数,原子和字符串)的引用表达式等于该值本身。其他值,如地图,需要明确转换。最后,函数和引用之类的值根本无法转换为带引号的表达式。
你可以阅读更多关于quote
与unquote
中Kernel.SpecialForms
模块。Macro.escape/1
与引用表达式相关的文档和其他功能可以在Macro
模块中找到。
在这篇介绍中,我们已经奠定了基础,最终编写我们的第一个宏,所以让我们转到下一章。