Elixir语言中 Kernel.SpecialForms.quote
相关用法介绍如下。
用法:
quote(opts, block)
(宏)
获取任何表达式的表示。
例子
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
Elixir 的 AST(抽象语法树)
任何 Elixir 代码都可以使用 Elixir 数据结构来表示。 Elixir 宏的构建块是一个包含三个元素的元组,例如:
{:sum, [], [1, 2, 3]}
上面的元组表示对 sum
的函数调用,将 1、2 和 3 作为参数传递。元组元素是:
-
元组的第一个元素总是一个原子或相同表示中的另一个元组。
-
元组的第二个元素表示 metadata 。
-
元组的第三个元素是函数调用的参数。第三个参数可能是一个原子,它通常是一个变量(或本地调用)。
除了上述元组之外,Elixir 还有一些文字也是其 AST 的一部分。这些文字在引用时会自行返回。他们是:
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
任何其他值,例如映射或 four-element 元组,必须在被引入 AST 之前进行转义 (
)。Macro.escape/1
选项
-
:bind_quoted
- 将绑定传递给宏。无论何时给出绑定,unquote/1
-
:context
- 设置分辨率上下文。 -
:generated
- 将给定的块标记为已生成,因此它不会发出警告。目前它仅适用于特殊形式(例如,您可以注释case
但不能注释if
)。 -
:file
- 将引用的表达式设置为具有给定文件。 -
:line
- 将引用的表达式设置为具有给定的行。 -
:location
- 当设置为:keep
时,保留当前行和文件的引用。阅读下面的"Stacktrace information" 部分了解更多信息。 -
:unquote
- 当false
时,禁用取消引用。这意味着任何unquote
调用都将在 AST 中保持原样,而不是被unquote
参数替换。例如:iex> quote do ...> unquote("hello") ...> end "hello" iex> quote unquote: false do ...> unquote("hello") ...> end {:unquote, [], ["hello"]}
引号和宏
通常与宏一起用于代码生成。作为练习,让我们定义一个将数字乘以自身(平方)的宏。在实践中,没有理由定义这样的宏(实际上这会被视为一种不好的做法),但它足够简单,可以让我们专注于引号和宏的重要方面:quote/2
defmodule Math do
defmacro squared(x) do
quote do
unquote(x) * unquote(x)
end
end
end
我们可以这样调用它:
import Math
IO.puts("Got #{squared(5)}")
起初,这个例子中没有任何东西可以真正揭示它是一个宏。但是正在发生的事情是,在编译时, squared(5)
变为 5 * 5
。 5
参数在生成的代码中重复,我们可以在实践中看到这种行为,因为我们的宏实际上有一个错误:
import Math
my_number = fn ->
IO.puts("Returning 5")
5
end
IO.puts("Got #{squared(my_number.())}")
上面的示例将打印:
Returning 5
Returning 5
Got 25
注意“Returning 5”是如何被打印两次的,而不是一次。这是因为宏接收一个表达式而不是一个值(这是我们在常规函数中所期望的)。这意味着:
squared(my_number.())
实际上扩展为:
my_number.() * my_number.()
它调用了两次函数,解释了为什么我们得到了两次打印的值!在大多数情况下,这实际上是出乎意料的行为,这就是为什么在涉及宏时需要记住的第一件事就是不要多次取消引用相同的值。
让我们修复我们的宏:
defmodule Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end
现在像以前一样调用squared(my_number.())
将只打印一次该值。
事实上,这种模式非常普遍,以至于大多数时候您都希望将 bind_quoted
选项与
一起使用:quote/2
defmodule Math do
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end
:bind_quoted
将转换为与上述示例相同的代码。 :bind_quoted
可以在很多情况下使用并且被视为良好实践,不仅因为它有助于防止我们遇到常见错误,还因为它允许我们利用宏公开的其他工具,例如在某些下面的部分。
在我们完成这个简短的介绍之前,您会注意到,即使我们在引用中定义了一个变量x
:
quote do
x = unquote(x)
x * x
end
当我们调用时:
import Math
squared(5)
x
** (CompileError) undefined variable x or undefined function x/0
我们可以看到x
没有泄漏到用户上下文。发生这种情况是因为 Elixir 宏是卫生的,我们将在下一节中详细讨论这个主题。
变量中的卫生
考虑以下示例:
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference()
a
#=> 10
在上面的示例中,a
返回 10,即使宏显然将其设置为 1,因为宏中定义的变量不会影响执行宏的上下文。如果要在调用者的上下文中设置或获取变量,您可以在var!
宏的帮助下完成:
defmodule NoHygiene do
defmacro interference do
quote do
var!(a) = 1
end
end
end
require NoHygiene
a = 10
NoHygiene.interference()
a
#=> 1
你甚至不能访问在同一个模块中定义的变量,除非你明确地给它一个上下文:
defmodule Hygiene do
defmacro write do
quote do
a = 1
end
end
defmacro read do
quote do
a
end
end
end
Hygiene.write()
Hygiene.read()
** (RuntimeError) undefined variable a or undefined function a/0
为此,您可以显式地将当前模块范围作为参数传递:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
ContextHygiene.write()
ContextHygiene.read()
#=> 1
别名卫生
默认情况下,引号内的别名是卫生的。考虑以下示例:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
Hygiene.no_interference()
#=> %{}
请注意,即使在宏展开的上下文中别名 M
不可用,上面的代码仍然有效,因为 M
仍然展开为
。Map
同样,即使我们在调用宏之前定义了同名的别名,也不会影响宏的结果:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}
在某些情况下,您希望访问调用者中定义的别名或模块。为此,您可以使用 alias!
宏:
defmodule Hygiene do
# This will expand to Elixir.Nested.hello()
defmacro no_interference do
quote do
Nested.hello()
end
end
# This will expand to Nested.hello() for
# whatever is Nested in the caller
defmacro interference do
quote do
alias!(Nested).hello()
end
end
end
defmodule Parent do
defmodule Nested do
def hello, do: "world"
end
require Hygiene
Hygiene.no_interference()
** (UndefinedFunctionError) ...
Hygiene.interference()
#=> "world"
end
导入卫生
与别名类似,Elixir 中的导入是卫生的。考虑以下代码:
defmodule Hygiene do
defmacrop get_length do
quote do
length([1, 2, 3])
end
end
def return_length do
import Kernel, except: [length: 1]
get_length
end
end
Hygiene.return_length()
#=> 3
请注意 Hygiene.return_length/0
如何返回 3
,即使未导入
函数。事实上,即使Kernel.length/1
return_length/0
从另一个模块导入同名同元的函数,也不会影响函数结果:
def return_length do
import String, only: [length: 1]
get_length
end
调用这个新的 return_length/0
仍将返回 3
作为结果。
Elixir 足够聪明,可以将解决方案延迟到可能的最新时刻。因此,如果您在引号内调用 length([1, 2, 3])
,但没有可用的
函数,则它会在调用者中展开:length/1
defmodule Lazy do
defmacrop get_length do
import Kernel, except: [length: 1]
quote do
length("hello")
end
end
def return_length do
import Kernel, except: [length: 1]
import String, only: [length: 1]
get_length
end
end
Lazy.return_length()
#=> 5
堆栈跟踪信息
通过宏定义函数时,开发人员可以选择是从调用者还是从引用内部报告运行时错误。让我们看一个例子:
# adder.ex
defmodule Adder do
@doc "Defines a function that adds two numbers"
defmacro defadd do
quote location: :keep do
def add(a, b), do: a + b
end
end
end
# sample.ex
defmodule Sample do
import Adder
defadd
end
require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
adder.ex:5: Sample.add/2
当使用 location: :keep
并且为 Sample.add/2
提供无效参数时,堆栈跟踪信息将指向引用内的文件和行。如果没有 location: :keep
,则会将错误报告到调用 defadd
的位置。 location: :keep
仅影响引号内的定义。
重要提示:不要使用 location: :keep 如果函数定义还 unquote
s 一些宏参数。如果这样做,Elixir 将存储当前位置的文件定义,但未引用的参数可能包含宏调用者的行信息,从而导致错误的堆栈跟踪。
绑定和取消引用片段
Elixir 引用/取消引用机制提供了一种称为取消引用片段的函数。取消引用片段提供了一种即时生成函数的简单方法。考虑这个例子:
kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
在上面的示例中,我们动态生成了函数foo/0
和bar/0
。现在,假设我们想将此函数转换为宏:
defmacro defkv(kv) do
Enum.map(kv, fn {k, v} ->
quote do
def unquote(k)(), do: unquote(v)
end
end)
end
我们可以调用这个宏:
defkv [foo: 1, bar: 2]
但是,我们不能如下调用它:
kv = [foo: 1, bar: 2]
defkv kv
这是因为宏在编译时期望它的参数是一个关键字列表。由于在上面的示例中,我们传递了变量 kv
的表示形式,因此我们的代码失败了。
这实际上是开发宏时的常见陷阱。我们在宏中假设一个特定的形状。我们可以通过在引用的表达式中取消引用变量来解决它:
defmacro defkv(kv) do
quote do
Enum.each(unquote(kv), fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
如果您尝试运行我们的新宏,您会发现它甚至无法编译,并抱怨变量k
和v
不存在。这是因为模棱两可:unquote(k)
可以像以前一样是取消引用的片段,也可以是像 unquote(kv)
中的常规取消引用。
此问题的一种解决方案是禁用宏中的取消引用,但是,这样做会导致无法将 kv
表示形式注入到树中。这就是:bind_quoted
选项来救援的时候(再次!)。通过使用 :bind_quoted
,我们可以自动禁用取消引用,同时仍将所需的变量注入到树中:
defmacro defkv(kv) do
quote bind_quoted: [kv: kv] do
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
事实上,每次想要在引号中注入一个值时,都建议使用 :bind_quoted
选项。
相关用法
- Elixir Kernel.SpecialForms.case用法及代码示例
- Elixir Kernel.SpecialForms.%{}用法及代码示例
- Elixir Kernel.SpecialForms.for用法及代码示例
- Elixir Kernel.SpecialForms.require用法及代码示例
- Elixir Kernel.SpecialForms.&expr用法及代码示例
- Elixir Kernel.SpecialForms.<<args>>用法及代码示例
- Elixir Kernel.SpecialForms.{args}用法及代码示例
- Elixir Kernel.SpecialForms.unquote_splicing用法及代码示例
- Elixir Kernel.SpecialForms.receive用法及代码示例
- Elixir Kernel.SpecialForms.%struct{}用法及代码示例
- Elixir Kernel.SpecialForms.import用法及代码示例
- Elixir Kernel.SpecialForms.left . right用法及代码示例
- Elixir Kernel.SpecialForms.alias用法及代码示例
- Elixir Kernel.SpecialForms.try用法及代码示例
- Elixir Kernel.SpecialForms.fn用法及代码示例
- Elixir Kernel.SpecialForms.cond用法及代码示例
- Elixir Kernel.SpecialForms.__aliases__用法及代码示例
- Elixir Kernel.SpecialForms.left :: right用法及代码示例
- Elixir Kernel.SpecialForms.unquote用法及代码示例
- Elixir Kernel.SpecialForms.with用法及代码示例
- Elixir Kernel.SpecialForms.__block__用法及代码示例
- Elixir Kernel.SpecialForms.^var用法及代码示例
- Elixir Kernel.round用法及代码示例
- Elixir Kernel.left / right用法及代码示例
- Elixir Kernel.put_in用法及代码示例
注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 Kernel.SpecialForms.quote(opts, block)。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。