(译) 理解 Elixir 中的宏 Macro, 第六部门:原地代码天生

锦通  金牌会员 | 2024-5-16 19:29:24 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 555|帖子 555|积分 1665

Elixir Macros 系列文章译文
这是宏系列文章的最后一篇. 在开始之前, 我想提一下 Björn Rochel, 他已经将他的 Apex 库中的 deftraceable 宏改进了. 因为他发现系列文章中 deftraceable 的版本不能精确处理默认参数(arg \ def_value), 于是做了一个修复 bugfix.
这次, 让我们竣事这个宏的故事. 今天的文章知识点可能是整个系列中涉及最广的, 我们将讨论原地代码天生的相干技术, 以及它可能对宏的影响.
在模块 module 中天生代码

正如我在第 1 章中提到的那样, 宏并不是 Elixir 中唯一的元编程机制. 我们也可以在模块中直接天生代码. 为了唤起你的记忆, 我们来看看下面的例子:
  1. defmodule Fsm do
  2.   fsm = [
  3.     running: {:pause, :paused},
  4.     running: {:stop, :stopped},
  5.     paused: {:resume, :running}
  6.   ]
  7.   # Dynamically generating functions directly in the module
  8.   for {state, {action, next_state}} <- fsm do
  9.     def unquote(action)(unquote(state)), do: unquote(next_state)
  10.   end
  11.   def initial, do: :running
  12. end
  13. Fsm.initial
  14. # :running
  15. Fsm.initial |> Fsm.pause
  16. # :paused
  17. Fsm.initial |> Fsm.pause |> Fsm.pause
  18. # ** (FunctionClauseError) no function clause matching in Fsm.pause/1
复制代码
在这里, 我们直接在模块中动态天生函数子句(clause). 这允许我们针对某些输入(在本例中是关键字列表)进行元编程, 并天生代码, 而无需编写专门的宏.
留意, 在上面的代码中, 我们怎样利用 unquote 将变量注入到函数子句定义中. 这与宏的工作方式完全同等. 请记住, def 也是一个宏, 并且宏接收的参数总是被 quoted. 因此, 假如您想要一个宏参数接收某个变量的值, 您必须在传递该变量时利用 unquote. 仅仅调用 def action 是不敷的, 因为 def 宏接收到的是对 action 的 unquoted, 而不是变量 action 中的值.
当然, 您可以以这种动态的方式调用本身的宏, 原理是一样的. 然而, 有一个意想不到的情况 — 天生(evaluation) 的顺序与你的预期可能不符.
展开的顺序

正如你所预料的那般, 模块级代码(不是任何函数的一部门的代码)在 Elixir 编译过程的展开阶段被执行. 有些令人意外的是, 这将发生在全部宏(除了 def)展开之后. 很轻易去证明这一点:
  1. iex(1)> defmodule MyMacro do
  2.           defmacro my_macro do
  3.             IO.puts "my_macro called"
  4.             nil
  5.           end
  6.         end
  7. iex(2)> defmodule Test do
  8.           import MyMacro
  9.           IO.puts "module-level expression"
  10.           my_macro
  11.         end
  12. # Output:
  13. my_macro called
  14. module-level expression
复制代码
从输出看出, 即使代码中相应的 IO.puts 调用在宏调用之前, 但 mymacro 还是在 IO.puts 之前被调用了. 这证明编译器首先剖析全部尺度宏. 然后开始天生模块, 也是在这个阶段, 模块级代码以及对 def 的调用被执行.
模块级友好宏

这对我们本身的宏有一些重要的影响. 例如, 我们的 deftraceable 宏也可以动态调用. 但是, 现在它还不能工作:
  1. iex(1)> defmodule Tracer do ... end
  2. iex(2)> defmodule Test do
  3.           import Tracer
  4.           fsm = [
  5.             running: {:pause, :paused},
  6.             running: {:stop, :stopped},
  7.             paused: {:resume, :running}
  8.           ]
  9.           for {state, {action, next_state}} <- fsm do
  10.             # Using deftraceable dynamically
  11.             deftraceable unquote(action)(unquote(state)), do: unquote(next_state)
  12.           end
  13.           deftraceable initial, do: :running
  14.         end
  15. ** (MatchError) no match of right hand side value: :error
  16.     expanding macro: Tracer.deftraceable/2
  17.     iex:13: Test (module)
复制代码
让我们试试这个宏:、
  1. defmacro deftraceable(head, body) do
  2.   # 这里, 我们假设输入头部是什么样的, 并执行一些操作
  3.   # AST 转换基于这些假设.
  4.   quote do
  5.     ...
  6.   end
  7. end
复制代码
正如你所看到的那样, 修改并不复杂. 我们想法保持我们的大部门代码完整, 虽然我们不得不用一些技巧:bind_quoted:true 和 Macro.escape:
  1. defmacro my_macro do
  2.   # Macro context(宏上下文): 这里的代码是宏的正常部分, 并在宏运行时被执行
  3.   quote do
  4.     # Caller's context(调用者上下文): 生成的代码在宏所在的位置运行
  5.   end
复制代码
让我们细致看看它们是什么意思.
bind_quoted

记住, 我们的宏天生一个代码, 它将天生最终的代码. 在第一级天生的代码(由我们的宏返回的代码)的某处, 我们需要放置以下表达式:
  1. defmacro deftraceable(head, body) do
  2.   # Macro context: 我们不应该对输入 AST 做任何假设
  3.   quote do
  4.     # Caller's context: 我们应该在这里转换输入的 AST, 然后在这里做出我们的假设
  5.   end
  6. end
复制代码
这个表达式将在调用者的上下文(客户端模块)中被调用, 它的任务是天生函数. 如在注释中提到的, 重要的是要理解 unquote(head) 在这里引用的是存在于调用者上下文中的 head 变量. 我们不是从宏上下文注入一个变量, 而是一个存在于调用者上下文中的变量.
但是, 我们不能利用简单的 quote 天生这样的表达式:
  1. quote do  defmacro deftraceable(head, body) do
  2.   # Macro context: 我们不应该对输入 AST 做任何假设
  3.   quote do
  4.     # Caller's context: 我们应该在这里转换输入的 AST, 然后在这里做出我们的假设
  5.   end
  6. endend
复制代码
记住 unquote 怎样工作. 它往 unquote 调用里的 head 变量中注入了 AST. 这不是我们想要的. 我们想要的是天生表现对 unquote 的调用的 AST, 然后在调用者的上下文中执行, 并引用调用者的 head 变量.
这可以通过提供 unquote:false 选项来实现:
  1. quote unquote: false do  defmacro deftraceable(head, body) do
  2.   # Macro context: 我们不应该对输入 AST 做任何假设
  3.   quote do
  4.     # Caller's context: 我们应该在这里转换输入的 AST, 然后在这里做出我们的假设
  5.   end
  6. endend
复制代码
这里, 我们将天生代表 unquote 调用的代码. 假如这个代码被注入到精确的地方, 且此中变量 head 存在, 我们将最终调用 def 宏, 传递 head 变量中的任何值.
所以似乎利用 unquote: false 可以达到我们想要的效果, 但有一个缺点, 我们不能从宏上下文访问任何变量:
  1. defmacro ...
  2.   # 宏上下文(Macro context):
  3.   # 可以在这里做任何准备工作,
  4.   # 只要不对输入的 AST 作任何假设
  5.   quote do
  6.     # 调用者上下文(Caller's context):
  7.     # 如果你需要分析或转换输入的 AST, 你应该在这里进行.
  8.   end
复制代码
如你所见, 我们乐成传送了未受影响的数据.
再看我们的耽误代码天生, 这正是我们需要的. 与注入目的 AST 不同, 我们想要传输输入 AST, 完全保留它的外形:
  1. defmodule Tracer do
  2.   defmacro deftraceable(head, body) do
  3.     # 这是最重要的更改, 让我们能正确传递
  4.     # 输入 AST 到调用者的上下文中. 我稍后会解释这是如何工作的.
  5.     quote bind_quoted: [
  6.       head: Macro.escape(head, unquote: true),
  7.       body: Macro.escape(body, unquote: true)
  8.     ] do
  9.       # Caller's context: 我们将从这里生成代码
  10.       # 由于代码生成被推迟到调用者上下文,
  11.       # 我们现在可以对输入 AST 做出我们的假设.
  12.       # 此代码大部分与以前的版本相同
  13.       #
  14.       # 注意, 这些变量现在在调用者的上下文中创建
  15.       {fun_name, args_ast} = Tracer.name_and_args(head)
  16.       {arg_names, decorated_args} = Tracer.decorate_args(args_ast)
  17.       # 与以前的版本完全相同.
  18.       head = Macro.postwalk(head,
  19.         fn
  20.           ({fun_ast, context, old_args}) when (
  21.             fun_ast == fun_name and old_args == args_ast
  22.           ) ->
  23.             {fun_ast, context, decorated_args}
  24.           (other) -> other
  25.       end)
  26.       # 此代码与以前的版本完全相同.
  27.       # Note: 但是, 请注意, 代码像前面三个表达式那样
  28.       # 在相同的上下文中执行.
  29.       #
  30.       # 因此,  unquote(head) 在这里引用了 head 变量
  31.       # 在此上下文中计算, 而不是宏上下文.
  32.       # 这同样适用于其它发生在函数体中的 unquote.
  33.       #
  34.       # 这就是延迟代码生成的意义所在. 我们的宏产生
  35.       # 此代码, 然后依次生成最终代码.
  36.       def unquote(head) do
  37.         file = __ENV__.file
  38.         line = __ENV__.line
  39.         module = __ENV__.module
  40.         function_name = unquote(fun_name)
  41.         passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
  42.         result = unquote(body[:do])
  43.         loc = "#{file}(line #{line})"
  44.         call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
  45.         IO.puts "#{loc} #{call}"
  46.         result
  47.       end
  48.     end
  49.   end
  50.   # 与前一个版本相同, 但函数被导出, 因为它们
  51.   # 必须从调用方的上下文中调用.
  52.   def name_and_args({:when, _, [short_head | _]}) do
  53.     name_and_args(short_head)
  54.   end
  55.   def name_and_args(short_head) do
  56.     Macro.decompose_call(short_head)
  57.   end
  58.   def decorate_args([]), do: {[],[]}
  59.   def decorate_args(args_ast) do
  60.     for {arg_ast, index} <- Enum.with_index(args_ast) do
  61.       arg_name = Macro.var(:"arg#{index}", __MODULE__)
  62.       full_arg = quote do
  63.         unquote(arg_ast) = unquote(arg_name)
  64.       end
  65.       {arg_name, full_arg}
  66.     end
  67.     |> Enum.unzip
  68.   end
  69. end
复制代码
通过利用 Macro.escape/1, 我们可以确保输入 AST 被原原本本地传输回调用者的上下文, 在那边我们将天生最终的代码.
正如前一节所讨论的, 我们利用了 bind_quoted, 但原理相同:
  1. iex(1)> defmodule Tracer do ... end
  2. iex(2)> defmodule Test do
  3.           import Tracer
  4.           fsm = [
  5.             running: {:pause, :paused},
  6.             running: {:stop, :stopped},
  7.             paused: {:resume, :running}
  8.           ]
  9.           for {state, {action, next_state}} <- fsm do
  10.             deftraceable unquote(action)(unquote(state)), do: unquote(next_state)
  11.           end
  12.           deftraceable initial, do: :running
  13.         end
  14. iex(3)> Test.initial |> Test.pause |> Test.resume |> Test.stop
  15. iex(line 15) Elixir.Test.initial() = :running
  16. iex(line 13) Elixir.Test.pause(:running) = :paused
  17. iex(line 13) Elixir.Test.resume(:paused) = :running
  18. iex(line 13) Elixir.Test.stop(:running) = :stopped
复制代码
Escaping 和 unquote: true

留意我们传递给了 Macro.escape 一个欺骗性的 unquote: true 选项. 这是最难解释的. 为了能够理解它, 你必须清楚 AST 是怎样传递给宏并返回到调用者的上下文中的.
首先, 记住我们怎样调用我们的宏:
  1. quote bind_quoted: [
  2.   head: Macro.escape(head, unquote: true),
  3.   body: Macro.escape(body, unquote: true)
  4. ] do
  5.   ...
  6. end
复制代码
现在, 由于宏实际上接收到的是 quoted 的参数, head 参数将等同于以下内容:
  1. def unquote(head) do ... end
复制代码
请记住, Macro.escape 会生存数据, 因此当你在其他 AST 中传输变量时, 其内容将保持不变. 思量下上面的 head 外形, 这是我们在宏展开后最终会出现的情况:
[code]# 调用者的上下文for {state, {action, next_state}}
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

锦通

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表