宏定义中的unquoting参数会挂起函数调用

时间:2016-10-10 02:24:07

标签: elixir

我一直在研究宏(Dave Thomas的优秀Programming Elixir 1.2,第21章),我对正在发生的事情有了一点了解。我有两个模块TracerTest,其中Tracer重新定义def宏以插入调用和响应日志记录,如下所示:

defmodule Tracer do
  def dump_args(args) do
    args |> Enum.map(&inspect/1) |> Enum.join(",")
  end

  def dump_defn(name, args) do
    "#{name}(#{dump_args(args)})"
  end

  defmacro def({name, _, args} = definition, do: content) do
    IO.puts("Definition: #{inspect definition}")
    IO.puts("Content:    #{inspect content}")
    quote do
      Kernel.def unquote(definition) do
        IO.puts("==> call:   #{Tracer.dump_defn(unquote(name), unquote(args))}")
        result = unquote(content)
        IO.puts("<== resp:   #{inspect result}")
        result
      end
    end
  end
end

Test使用此宏来演示其用途:

defmodule Test do
  import Kernel, except: [def: 2]
  import Tracer, only: [def: 2]

  def puts_sum_three(a, b, c), do: IO.inspect(a+b+c)
  def add_list(list), do: Enum.reduce(list, 0, &(&1 + &2))
  def neg(a) when a < 0, do: -a
end

执行此操作时,前两个函数按预期工作:

iex(3)> Test.puts_sum_three(1,2,3)
==> call:   puts_sum_three(1,2,3)
6
<== resp:   6
6
iex(4)> Test.add_list([1,2,3,4])
==> call:   add_list([1, 2, 3, 4])
<== resp:   10
10

然而,第三个函数似乎挂起 - 特别是挂起在unquote(args)

iex(5)> Test.neg(-1)

所以我的问题是,是什么原因导致对挂起的召唤?虽然我认为我有一个想法,但我想确认(或反驳)我对其原因的理解。

第三个函数中的when子句更改了传递给def宏的引用表达式的表示形式,并且我在重新处理宏时没有遇到任何问题。 definition传递的contentneg为:

Definition: {:when, [line: 7], [{:neg, [line: 7], [{:a, [line: 7], nil}]}, {:<, [line: 7], [{:a, [line: 7], nil}, 0]}]}
Content:    {:-, [line: 7], [{:a, [line: 7], nil}]}

当我们到达unquote(args)中的neg时,它正在尝试评估args表达式,我认为该表达式包含对neg的调用并导致无限递归循环。它是否正确?任何有关我如何调试/诊断这一点的指示也将受到赞赏,以及进一步阅读的链接。

1 个答案:

答案 0 :(得分:3)

  

当我们到达unquote(args)中的neg时,它正在尝试评估args表达式,我认为该表达式包含对neg的调用并导致无限递归循环。这是对的吗?

是的,这就是该行编译的内容:

IO.puts("==> call:   #{Tracer.dump_defn(:when, [neg(a), a < 0])}")

导致无限递归。

  

关于如何调试/诊断这一点的任何指示也将受到赞赏,以及进一步阅读的链接。

一种方法是将quote返回的值传递给|> Macro.to_string |> IO.puts。这将打印由quote表达式生成的确切代码。这正是我为确认你的假设所做的:

defmacro def({name, _, args} = definition, do: content) do
  IO.puts("Definition: #{inspect definition}")
  IO.puts("Content:    #{inspect content}")
  ast = quote do
    Kernel.def unquote(definition) do
      IO.puts("==> call:   #{Tracer.dump_defn(unquote(name), unquote(args))}")
      result = unquote(content)
      IO.puts("<== resp:   #{inspect result}")
      result
    end
  end
  ast |> Macro.to_string |> IO.puts
  ast
end

打印:

Kernel.def(neg(a) when a < 0) do
  IO.puts("==> call:   #{Tracer.dump_defn(:when, [neg(a), a < 0])}")
  result = -a
  IO.puts("<== resp:   #{inspect(result)}")
  result
end

明确了该函数挂起的原因。

解决此问题的一种方法是,如果名称为:when,则提取实际名称/ args:

...
{name, args} =
  case {name, args} do
    {:when, [{name, _, args}, _]} -> {name, args}
    {name, args} -> {name, args}
  end
quote do

在此之后,Test.neg/1起作用:

iex(1)> Test.neg -1
==> call:   neg(-1)
<== resp:   1
1
iex(2)> Test.neg -100
==> call:   neg(-100)
<== resp:   100
100
  ...