(译) 理解 Elixir 中的宏 Macro, 第五部分:组装 AST

打印 上一主题 下一主题

主题 531|帖子 531|积分 1593

Elixir Macros 系列文章译文
上次我介绍了一个基本版本的可追溯宏 deftraceable, 它允许我们编写可跟踪的函数. 这个宏的最终版本还有一些遗留的问题, 今天我们将办理其中一个 — 参数模式匹配.
从今天的练习应该认识到, 我们必须细致思量关于宏大概吸收到的输入的所有假设情况.
问题所在

正如我上次所暗示的那样, 当前版本的 deftraceable 不能使用模式匹配的参数. 让我们来演示一下这个问题:
  1. iex(1)> defmodule Tracer do ... end
  2. iex(2)> defmodule Test do
  3.           import Tracer
  4.           deftraceable div(_, 0), do: :error
  5.         end
  6. ** (CompileError) iex:5: unbound variable _
复制代码
发生了什么? deftraceable 宏盲目地假设输入参数是普通变量或常量. 因此, 当你调用 deftracable div(a, b) 时, deftracable div(a, b), do: ... 生成的代码将包含:
  1. passed_args = [a, b] |> Enum.map(&inspect/1) |> Enum.join(",")
复制代码
上面这段会按预期工作, 但如果一个参数是匿名变量(_), 那么我们将生成以下代码:
  1. passed_args = [_, 0] |> Enum.map(&inspect/1) |> Enum.join(",")
复制代码
这显然是不正确的, 因此我们得到了未绑定变量错误.
那么办理方案是什么呢? 我们不应该对输入参数做任何假设. 相反, 我们应该将每个参数放入宏生成的专用变量中. 或者用代码来表达, 如果宏被调用:
  1. deftraceable fun(pattern1, pattern2, ...)
复制代码
我们会生成如许的函数头:
  1. def fun(pattern1 = arg1, pattern2 = arg2, ...)
复制代码
这将允许我们将参数值代入内部暂时变量, 并打印这些变量的内容.
办理方案

让我们来实现它. 起首, 我将向你展示办理方案的顶层示意版:
  1. defmacro deftraceable(head, body) do
  2.   {fun_name, args_ast} = name_and_args(head)
  3.   # 通过给每个参数添加 "= argX"来装饰输入参数.
  4.   # 返回参数名称列表 (arg1, arg2, ...)
  5.   {arg_names, decorated_args} = decorate_args(args_ast)
  6.   head = ??   # Replace original args with decorated ones
  7.   quote do
  8.     def unquote(head) do
  9.       ... # 不变
  10.       # 使用临时变量构造追踪信息
  11.       passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
  12.       ... # 不变
  13.     end
  14.   end
  15. end
复制代码
起首, 我们从函数头(head)提取函数名称和 args (我们在前一篇文章中办理了这个问题).  然后, 我们必须将 = argX 注入到 args_ast 中, 并收回修改后的参数(我们将将其放入 decorated_args中).
我们还需要生成的变量的纯名称(或者更确切地说是它们的 AST), 由于我们将使用这些名称来收集参数值. 变量 arg_names 现实上包含 quote do [arg_1, arg_2, ....] end, 可以很容易地注入到 AST 树中.
我们来实现剩下的部分. 起首, 让我们看看如何修饰参数:
  1. defp decorate_args(args_ast) do
  2.   for {arg_ast, index} <- Enum.with_index(args_ast) do
  3.     # 动态生成 quoted 标识符
  4.     arg_name = Macro.var(:"arg#{index}", __MODULE__)
  5.     # 为 patternX = argX 生成 AST
  6.     full_arg = quote do
  7.       unquote(arg_ast) = unquote(arg_name)
  8.     end
  9.     {arg_name, full_arg}
  10.   end
  11.   |> Enum.unzip
  12. end
复制代码
大多数操作发生在 for 语句中. 本质上, 我们处理了每个变量输入的 AST 片断, 然后使用 Macro.var/2 函数计算暂时名称(quoted 的 argX), 它能将一个原子变换成一个名称与其相同的 quoted 的变量. Macro.var/2 的第二个参数确保变量是hygienic 的. 尽管我们将 arg1, arg2, ... 变量注入到调用者上下文中, 但调用者不会看到这些变量. 究竟上, deftraceable 的用户可以自由地使用这些名称作为一些局部变量, 不会干扰我们的宏引入的暂时变量.
末了, 在推导式的末尾, 我们返回一个元组, 该元组由暂时的名称和 quoted 的完整模式构成 - (例如 _ = arg1, 或 0 = arg2). 使用 unzip 和 to_tuple 进行推导之后确保 decorate_args以 {arg_names, decorated_args} 的形式返回结果.
decorate_args 辅助变量就绪后, 我们就可以传递输入参数, 并获得修饰参数, 以及暂时变量的名称. 现在我们需要将这些修饰过的参数注入到函数的头部, 以取代原始参数. 要注意, 我们需要做到以下几点:

  • 递归遍历输入函数头的 AST
  • 找到指定函数名和参数的位置
  • 用修饰过的参数的 AST 更换原始(输入)参数
如果我们使用宏, Macro.postwalk/2 这个处理可以被公道地简化掉:
  1. defmacro deftraceable(head, body) do
  2.   {fun_name, args_ast} = name_and_args(head)
  3.   {arg_names, decorated_args} = decorate_args(args_ast)
  4.   # 1. 递归地遍历 AST
  5.   head = Macro.postwalk(
  6.     head,
  7.     # lambda 函数处理输入 AST 中的元素, 返回修改过的 AST
  8.     fn
  9.       # 2. 模式匹配函数名和参数所在的位置
  10.       ({fun_ast, context, old_args}) when (
  11.         fun_ast == fun_name and old_args == args_ast
  12.       ) ->
  13.         # 3. 将输入参数替换为修饰参数的 AST
  14.         {fun_ast, context, decorated_args}
  15.       # 头部 AST 中的其它元素(可能是 guards)
  16.       #   -> 我们让它保留不变
  17.       (other) -> other
  18.     end
  19.   )
  20.   ... # 不变
  21. end
复制代码
Macro.postwalk/2 递归地遍历 AST, 并且在所有节点的后代被访问之后, 为每个节点调用提供的 lambda 函数. lambda 函数吸收元素的 AST, 如许我们有机会返回一些除了指定节点之外的东西.
我们在这个 lambda 里做的现实上是一个模式匹配, 我们在寻找 {fun_name, context, args}. 如第三篇文章中所述那样, 这是表达式 some_fun(arg1, arg2, ...) 的 quoted 体现形式. 一旦我们碰到匹配此模式的节点, 我们只需要用新的(修饰过的)输入参数更换掉旧的. 在所有别的情况下, 我们简单地返回输入的 AST, 使得树的其余部分不变.
这看着有点复杂了, 但它办理了我们的问题. 以下是 deftraceable 宏的最终版本:
  1. defmodule Tracer do
  2.   defmacro deftraceable(head, body) do
  3.     {fun_name, args_ast} = name_and_args(head)
  4.     {arg_names, decorated_args} = decorate_args(args_ast)
  5.     head = Macro.postwalk(head,
  6.       fn
  7.         ({fun_ast, context, old_args}) when (
  8.           fun_ast == fun_name and old_args == args_ast
  9.         ) ->
  10.           {fun_ast, context, decorated_args}
  11.         (other) -> other
  12.       end)
  13.     quote do
  14.       def unquote(head) do
  15.         file = __ENV__.file
  16.         line = __ENV__.line
  17.         module = __ENV__.module
  18.         function_name = unquote(fun_name)
  19.         passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
  20.         result = unquote(body[:do])
  21.         loc = "#{file}(line #{line})"
  22.         call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
  23.         IO.puts "#{loc} #{call}"
  24.         result
  25.       end
  26.     end
  27.   end
  28.   defp name_and_args({:when, _, [short_head | _]}) do
  29.     name_and_args(short_head)
  30.   end
  31.   defp name_and_args(short_head) do
  32.     Macro.decompose_call(short_head)
  33.   end
  34.   defp decorate_args([]), do: {[],[]}
  35.   defp decorate_args(args_ast) do
  36.     for {arg_ast, index} <- Enum.with_index(args_ast) do
  37.       # 动态生成 quoted 标识符(identifier)
  38.       arg_name = Macro.var(:"arg#{index}", __MODULE__)
  39.       # 为 patternX = argX 构建 AST
  40.       full_arg = quote do
  41.         unquote(arg_ast) = unquote(arg_name)
  42.       end
  43.       {arg_name, full_arg}
  44.     end
  45.     |> Enum.unzip
  46.   end
  47. end
复制代码
让我们来试试:
  1. iex(1)> defmodule Tracer do ... end
  2. iex(2)> defmodule Test do
  3.           import Tracer
  4.           deftraceable div(_, 0), do: :error
  5.           deftraceable div(a, b), do: a/b
  6.         end
  7. iex(3)> Test.div(5, 2)
  8. iex(line 6) Elixir.Test.div(5,2) = 2.5
  9. iex(4)> Test.div(5, 0)
  10. iex(line 5) Elixir.Test.div(5,0) = :error
复制代码
正如你所看到的那样, 可以进入 AST, 分解它, 并在其中散布一些自定义的注入代码, 这并不算很复杂. 缺点是, 编写的宏的代码会变得越来越复杂, 并且更难分析.
今天的话题到此竣事. 下一次, 我将讨论原地代码生成技能 《(译) Understanding Elixir Macros, Part 6 - In-place Code Generation》.
原文: https://www.theerlangelist.com/article/macros_5
本文由博客群发一文多发等运营工具平台 OpenWrite 发布

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

水军大提督

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

标签云

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