上一章我们提出了一个基本版的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的名称和引用的完整模式——(例如_ = arg10 = arg2)所组成的元组。在最后使用unzipto_tuple确保了decorate_args{arg_names, decorated_args}的形式返回结果。

有了decorated_argshelper,我们可以传递输入参数,获得修饰好的值,包含临时变量的名称。现在我们需要将这些修饰好的参数插入函数的头部,替换掉原始的参数。特别地,我们必须执行以下步骤:

  1. 递归遍历输入函数头的AST。

  2. 查找指定函数名和参数的位置。

  3. 将原始(输入)参数替换为修饰好的参数的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.


Ljzn
399 声望102 粉丝

网络安全;函数式编程;数字货币;人工智能