【问题标题】:Unquoting argument in macro definition hangs function call在宏定义中取消引用参数会挂起函数调用
【发布时间】:2016-10-10 12:55:41
【问题描述】:

我一直在进行宏练习(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)

所以我的问题是,是什么导致调用 neg 挂起?虽然我认为我有一个想法,但我想确认(或反驳)我对它为什么这样做的理解。

第三个函数中的when 子句更改了传递给def 宏的带引号的表达式的表示形式,我在修改宏来处理这个问题时没有任何问题。为neg 传递的definitioncontent 是:

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}]}

当我们到达neg 中的unquote(args) 时,它正在尝试评估args 表达式,我认为这是一个包含对neg 的调用的列表,并导致无限递归循环。这个对吗?任何有关我如何调试/诊断此问题的指针以及指向进一步阅读的链接也将不胜感激。

【问题讨论】:

    标签: elixir


    【解决方案1】:

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

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

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

    导致无限递归。

    任何有关我如何调试/诊断此问题的指针以及指向进一步阅读的链接也将不胜感激。

    一种方法是将quote 返回的值传递给|&gt; Macro.to_string |&gt; 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 时提取实际名称/参数:

    ...
    {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
      ...
    

    【讨论】:

    • 谢谢,您提出的解决方案实际上是我解决它的方法,所以这让我有信心开始了解发生了什么!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-07
    相关资源
    最近更新 更多