在上一章中,我们展示了一些分析和处理输入的AST的基本方法。今天我们来看一些更复杂的AST转换。主要是重复一些已经解释过的技术。目的是表明,深入AST并不是很难,虽然结果的代码会很容易地变得相当复杂并有点hacky。

跟踪函数调用

在本文中,我们将创建一个deftraceable宏,它使我们能够定义可跟踪的函数。可跟踪函数的工作方式与正常函数相同,但是每当我们调用它时,都会打印出调试信息。这是它的作用:

defmodule Test do
  import Tracer

  deftraceable my_fun(a,b) do
    a/b
  end
end

Test.my_fun(6,2)

# => test.ex(line 4) Test.my_fun(6,2) = 3

这个例子当然是虚构的。你不需要设计这样的宏,因为Erlang已经有非常强大的跟踪功能,而且有一个Elixir包装可用。然而,这个例子很有趣,因为它需要一些更深入的AST转换和技术。

在开始之前,我要再提一次,你应该仔细考虑你是否真的需要这样的结构。例如deftraceable这样的宏引入了一个每个代码维护者都需要了解的东西。看着代码,它背后发生的事不是显而易见的。如果每个人都设计这样的结构,每个Elixir项目都会很快地变成自定义语言的大锅汤。当代码主要依赖于复杂的宏时,即使对于有经验的开发人员,也很难理解基本代码的流程。

但是在适合使用宏的情况下,你不应该仅仅因为有人声称宏是不好的,就不适用它。例如,如果在Erlang中没有跟踪功能,我们就需要设计一些宏来帮助我们(实际上不需要类似上述的例子,但那是另外一个话题),否则我们的代码就会有大量的重复模板。

在我看来,模板是不好的,因为代码中有了太多形式化的噪音,因此更难阅读和理解。宏有助于减少形式,但在使用宏之前,请先考虑是否可以使用运行时结构(函数,模块,协议)来解决重复。

看完这个长长的免责声明,让我们开始写deftraceable吧。首先,手动生成对应的代码。

让我们回忆一下用法:

deftraceable my_fun(a,b) do
  a/b
end

生成的代码如下所示:

def my_fun(a, b) do
  file = __ENV__.file
  line = __ENV__.line
  module = __ENV__.module
  function_name = "my_fun"
  passed_args = [a,b] |> Enum.map(&inspect/1) |> Enum.join(",")

  result = a/b

  loc = "#{file}(line #{line})"
  call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
  IO.puts "#{loc} #{call}"

  result
end

想法很简单。我们从编译器环境中获取各种数据,然后计算结果,最后将所有内容打印到屏幕上。

代码依赖于__ENV__特殊形式,可以用于在最终AST中注入所有类型的编译时信息(例如行号和文件)。__ENV__是一个结构,每当你在代码中使用它,它将在编译时扩展到适当的值。因此,无论我们在哪里写__ENV__.file,结果都是包含了文件名的(二进制)字符串常量。

现在我们需要动态构建这个代码。让我们来看看基本的大纲:

defmacro deftraceable(??) do
  quote do
    def unquote(head) do
      file = __ENV__.file
      line = __ENV__.line
      module = __ENV__.module
      function_name = ??
      passed_args = ?? |> Enum.map(&inspect/1) |> Enum.join(",")

      result = ??

      loc = "#{file}(line #{line})"
      call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
      IO.puts "#{loc} #{call}"

      result
    end
  end
end

这里我们在需要动态注入AST片段的地方放置问号(??),基于输入参数。特别是,我们必须从传递的参数中推导出函数名,参数名和函数体。

现在,当我们调用宏deftraceable my_fun(…) do … end,宏接收两个参数——函数头(函数名和参数列表)和包含函数体的关键字列表。。这两个当然都是被引用的。

我是如何知道的?其实我不知道。我一般通过尝试和错误获得的这些知识。基本上,我从定义一个宏开始:

defmacro deftraceable(arg1) do
  IO.inspect arg1
  nil
end

然后我尝试从一些测试模块或shell中调用宏。我将通过向宏定义中添加另一个参数来测试。一旦我得到结果,我会试图找出参数的表示是什么,然后开始构建宏。

宏结束处的nil确保我们不生成任何东西(我们生成的nil通常与调用者代码无关)。这允许我进一步构建片段而不注入代码。我通常依靠IO.inspectMacro.to_string/1来验证中间结果,一旦我满意了,我会删除nil部分,看看是否能工作。

此时deftraceable接收函数头和身体。函数头将是一个我们之前描述的格式的AST片段({function_name, context, [arg1, arg2, …])。

所以接下来我们需要:

  • 从引用的头中提取函数名和参数

  • 将这些值注入我们从宏返回的AST中

  • 将函数体注入同一个AST

  • 打印跟踪信息

我们可以使用模式匹配从这个AST片段中提取函数名和参数,但这里已经有一个helperMacro.decompose_call/1的功能正是这样。做完这些步骤,宏的最终版本如下所示:

defmodule Tracer do
  defmacro deftraceable(head, body) do
    # Extract function name and arguments
    {fun_name, args_ast} = Macro.decompose_call(head)

    quote do
      def unquote(head) do
        file = __ENV__.file
        line = __ENV__.line
        module = __ENV__.module

        # Inject function name and arguments into AST
        function_name = unquote(fun_name)
        passed_args = unquote(args_ast) |> Enum.map(&inspect/1) |> Enum.join(",")

        # Inject function body into the AST
        result = unquote(body[:do])

        # Print trace info"
        loc = "#{file}(line #{line})"
        call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
        IO.puts "#{loc} #{call}"

        result
      end
    end
  end
end

让我们来试试:

iex(1)> defmodule Tracer do ... end

iex(2)> defmodule Test do
          import Tracer

          deftraceable my_fun(a,b) do
            a/b
          end
        end

iex(3)> Test.my_fun(10,5)
iex(line 4) Test.my_fun(10,5) = 2.0   # trace output
2.0

它似乎可以工作。然而,我应该立刻指出这个实现中的问题:

  • 宏没有很好地处理guard

  • 模式匹配参数不会总是有效(例如,当使用 _ 去匹配任何值)

  • 当这模块中直接动态生成代码时,宏不工作

我将逐一解释每个问题,从guard语句开始,剩下的问题将在后面的文章中讨论。

掌控guards

所有与deftraceable有关的问题源于我们对输入的AST做出的一些假设。这是一个危险的领域,我们必须把所有情形都覆盖到。

例如,宏假定头部只包含名称和参数列表。因此,如果我们定义一个带有guard语句的可跟踪函数,deftraceable将不工作:

deftraceable my_fun(a,b) when a < b do
  a/b
end

在这种情况下,我们的头(宏的第一个参数)也将包含guard语句的信息,并且不能被Macro.decompose_call/1所解析。解决办法是检测这种情形,并以特殊方式处理它。

首先,让我们了解这个头是如何被引用的:

iex(1)> quote do my_fun(a,b) when a < b end
{:when, [],
 [{:my_fun, [], [{:a, [], Elixir}, {:b, [], Elixir}]},
  {:<, [context: Elixir, import: Kernel],
   [{:a, [], Elixir}, {:b, [], Elixir}]}]}

因此,本质上,我们的guard语句头具有{:when, _, [name_and_args, …]}的形状。我们可以依靠它来使用模式匹配来提取名称和参数:

defmodule Tracer do
  ...
  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
  ...

当然,我们需要从宏中调用此函数:

defmodule Tracer do
  ...
  defmacro deftraceable(head, body) do
    {fun_name, args_ast} = name_and_args(head)

    ... # unchanged
  end
  ...
end

如你所见,可以定义更多的私有函数并从你的宏中调用它们。毕竟,宏只是一个函数,当它被调用时,包含的模块已经被编译并加载到了编译器的VM中(否则,宏不能运行)。

以下是宏的完整版本:

defmodule Tracer do
  defmacro deftraceable(head, body) do
    {fun_name, args_ast} = name_and_args(head)

    quote do
      def unquote(head) do
        file = __ENV__.file
        line = __ENV__.line
        module = __ENV__.module

        function_name = unquote(fun_name)
        passed_args = unquote(args_ast) |> 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
end

让我们来试一下:

iex(1)> defmodule Tracer do ... end

iex(2)> defmodule Test do
          import Tracer

          deftraceable my_fun(a,b) when a<b do
            a/b
          end

          deftraceable my_fun(a,b) do
            a/b
          end
        end

iex(3)> Test.my_fun(5,10)
iex(line 4) Test.my_fun(10,5) = 2.0
2.0

iex(4)> Test.my_fun(10, 5)
iex(line 7) Test.my_fun(10,5) = 2.0

这个练习的要点是说明可以从输入的AST中推导出一些东西。在这个例子中,我们设法检测和处理一个函数guard。显然,代码变得更加复杂,因为它依赖于AST的内部结构。在这种情况下,代码依旧比较简单,但你将在后面的文章中看到我是如何解决deftraceable剩余的问题的,事情可能很快变得凌乱了。

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 粉丝

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