当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Elixir Kernel.SpecialForms.quote用法及代码示例


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 * 55 参数在生成的代码中重复,我们可以在实践中看到这种行为,因为我们的宏实际上有一个错误:

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/0bar/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

如果您尝试运行我们的新宏,您会发现它甚至无法编译,并抱怨变量kv 不存在。这是因为模棱两可: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-lang.org大神的英文原创作品 Kernel.SpecialForms.quote(opts, block)。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。