[elixir! #0007] [译] 理解Elixir中的宏——part.5 重塑AST by Saša Jurić
上一章我们提出了一个基本版的deftraceable
宏,能让我们编写可跟踪的函数。宏的最终版本有一些剩余的问题,今天我们将解决其中的一个——参数模式匹配。
今天的练习表明我们必须仔细考虑宏可能接收到的输入。
问题
正如我上一次暗示的那样,当前版本的deftraceable
不适用于模式匹配的参数。让我们来演示一下这个问题:
iex(1)> defmodule Tracer do ... end iex(2)> defmodule Test do import Tracer deftraceable div(_, 0), do: :error end ** (CompileError) iex:5: unbound variable _
发生了什么?deftraceable
宏盲目地将输入的参数当做是纯变量或常量。因此,当你调用deftraceable div (a, b), do: …
生成的代码会包含:
passed_args = [a, b] |> Enum.map(&inspect/1) |> Enum.join(",")
这将按预期工作,但如果一个参数是匿名变量(_
),那么我们将生成以下代码:
passed_args = [_, 0] |> Enum.map(&inspect/1) |> Enum.join(",")
这显然是不正确的,而且我们因此得到了未绑定变量的错误。
那么如何解决呢?我们不应该就输入参数做任何假设。相反,我们应该将每个参数转换为由宏生成的专用变量。如果我们的宏被调用,那么:
deftraceable fun(pattern1, pattern2, ...)
我们应该生成函数头:
def fun(pattern1 = arg1, pattern2 = arg2, ...)
这允许我们将参数值接收到我们的内部临时变量中,并打印这些变量的内容。
解决方法
让我们开始实现。首先,我将向你展示解决方案的顶层草图:
defmacro deftraceable(head, body) do {fun_name, args_ast} = name_and_args(head) # Decorates input args by adding "= argX" to each argument. # Also returns a list of argument names (arg1, arg2, ...) {arg_names, decorated_args} = decorate_args(args_ast) head = ?? # Replace original args with decorated ones quote do def unquote(head) do ... # unchanged # Use temp variables to make a trace message passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",") ... # unchanged end end end
首先,我们从头中提取名称和参数(我们在前一篇文章中已经解决了)。然后,我们必须在args_ast
中注入= argX
,并取回修改过的args(我们会将其放入decorated_args
)。
我们还需要生成的变量的纯名称(或者更确切地说,它们的AST),因为我们将使用这些变量来收集参数值。变量arg_names
本质上包含quote do [arg_1, arg_2, …] end
,可以很容易地注入到语法树中。
现在让我们实现其余的。首先,让我们看看如何装饰参数:
defp decorate_args(args_ast) do for {arg_ast, index} <- Enum.with_index(args_ast) do # Dynamically generate quoted identifier arg_name = Macro.var(:"arg#{index}", __MODULE__) # Generate AST for patternX = argX full_arg = quote do unquote(arg_ast) = unquote(arg_name) end {arg_name, full_arg} end |> List.unzip |> List.to_tuple end
大多数操作发生在for
语句中。本质上,我们经过了每个变量输入的AST片段,然后使用Macro.var/2
函数计算临时名称(引用的argX
),它能将一个原子变换成一个名称与其相同的引用的变量。Macro.var/2
的第二个参数确保变量是卫生的。尽管我们将arg1
,arg2
,…变量注入到调用者上下文中,但调用者不会看到这些变量。事实上,deftraceable
的用户可以自由地使用这些名称作为一些局部变量,不会干扰我们的宏引入的临时变量。
最后,在语境结束时,我们返回一个由temp的名称和引用的完整模式——(例如_ = arg1
或0 = arg2
)所组成的元组。在最后使用unzip
和to_tuple
确保了decorate_args
以{arg_names, decorated_args}
的形式返回结果。
有了decorated_args
helper,我们可以传递输入参数,获得修饰好的值,包含临时变量的名称。现在我们需要将这些修饰好的参数插入函数的头部,替换掉原始的参数。特别地,我们必须执行以下步骤:
递归遍历输入函数头的AST。
查找指定函数名和参数的位置。
将原始(输入)参数替换为修饰好的参数的AST
如果我们使用Macro.postwalk/2
函数,这个任务就可以合理地简化:
defmacro deftraceable(head, body) do {fun_name, args_ast} = name_and_args(head) {arg_names, decorated_args} = decorate_args(args_ast) # 1. Walk recursively through the AST head = Macro.postwalk( head, # This lambda is called for each element in the input AST and # has a chance of returning alternative AST fn # 2. Pattern match the place where function name and arguments are # specified ({fun_ast, context, old_args}) when ( fun_ast == fun_name and old_args == args_ast ) -> # 3. Replace input arguments with the AST of decorated arguments {fun_ast, context, decorated_args} # Some other element in the head AST (probably a guard) # -> we just leave it unchanged (other) -> other end ) ... # unchanged end
Macro.postwalk/2
递归地遍历AST,并且在所有节点的后代被访问之后,调用为每个节点提供的lambda。lambda接收元素的AST,这样我们有机会返回一些除了那个节点之外的东西。
我们在这个lambda里做的基本上是一个模式匹配,我们在寻找{fun_name, context, args}
。如第三章中所述,这是表达式some_fun(arg1, arg2, …)
的引用表示。一旦我们遇到匹配此模式的节点,我们只需要用新的(修饰的)输入参数替换掉旧的。在所有其它情况下,我们简单地返回输入的AST,使得树的其余部分不变。
这有点复杂,但它解决了我们的问题。以下是追踪宏的最终版本:
defmodule Tracer do defmacro deftraceable(head, body) do {fun_name, args_ast} = name_and_args(head) {arg_names, decorated_args} = decorate_args(args_ast) head = Macro.postwalk(head, fn ({fun_ast, context, old_args}) when ( fun_ast == fun_name and old_args == args_ast ) -> {fun_ast, context, decorated_args} (other) -> other end) quote do def unquote(head) do file = __ENV__.file line = __ENV__.line module = __ENV__.module function_name = unquote(fun_name) passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",") result = unquote(body[:do]) loc = "#{file}(line #{line})" call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}" IO.puts "#{loc} #{call}" result end end end defp name_and_args({:when, _, [short_head | _]}) do name_and_args(short_head) end defp name_and_args(short_head) do Macro.decompose_call(short_head) end defp decorate_args([]), do: {[],[]} defp decorate_args(args_ast) do for {arg_ast, index} <- Enum.with_index(args_ast) do # dynamically generate quoted identifier arg_name = Macro.var(:"arg#{index}", __MODULE__) # generate AST for patternX = argX full_arg = quote do unquote(arg_ast) = unquote(arg_name) end {arg_name, full_arg} end |> List.unzip |> List.to_tuple end end
来试验一下:
iex(1)> defmodule Tracer do ... end iex(2)> defmodule Test do import Tracer deftraceable div(_, 0), do: :error deftraceable div(a, b), do: a/b end iex(3)> Test.div(5, 2) iex(line 6) Elixir.Test.div(5,2) = 2.5 iex(4)> Test.div(5, 0) iex(line 5) Elixir.Test.div(5,0) = :error
正如你看到的,进入AST,把它分开,然后注入一些自定义的代码,这是可能的,而且不是非常复杂。缺点就是,生成宏的代码变得越来越复杂,并且更难分析。
本章到此结束。下一章我将讨论现场生成代码。
Copyright 2014, Saša Jurić. This article is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
The article was first published on the old version of the Erlangelist site.
The source of the article can be found here.