我一直在研究宏(Dave Thomas的优秀Programming Elixir 1.2,第21章),我对正在发生的事情有了一点了解。我有两个模块Tracer
和Test
,其中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
传递的content
和neg
为:
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
的调用并导致无限递归循环。它是否正确?任何有关我如何调试/诊断这一点的指示也将受到赞赏,以及进一步阅读的链接。
答案 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
...