2
votes

I've been working through an exercise on macros (Dave Thomas' excellent Programming Elixir 1.2, chapter 21) and I've hit a bit of a bump in my understanding of what's happening. I have two modules, Tracer and Test, where Tracer redefines the def macro to insert call and response logging as follows:

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

and Test uses this macro to demonstrate its use:

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

When executing this, the first two functions work as expected:

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

However, the third function appears to hang - specifically it hangs at unquote(args):

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

So my question is, what causes the call to neg to hang? While I think I have an idea, I'd like to confirm (or refute) my understanding of why it does so.

The when clause in the third function changes the representation of the quoted expression that is passed to the def macro and I have no problems in reworking the macro to handle this. The definition and content passed for neg are:

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

When we reach the unquote(args) in neg, it is attempting to evaluate the args expression, which I believe is a list containing a call to neg and results in an infinite recursive loop. Is this correct? Any pointers to how I might debug/diagnose this would also be appreciated, as would links to further reading.

1

1 Answers

3
votes

When we reach the unquote(args) in neg, it is attempting to evaluate the args expression, which I believe is a list containing a call to neg and results in an infinite recursive loop. Is this correct?

Yes, this is what that line compiles to:

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

which results in infinite recursion.

Any pointers to how I might debug/diagnose this would also be appreciated, as would links to further reading.

One way is to pass the value returned by quote to |> Macro.to_string |> IO.puts. This will print the exact code that was generated by that quote expression. This is exactly what I did to confirm your hypothesis:

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

This prints:

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

which makes it clear why the function hangs.


One way to fix this would be to extract the actual name/args if the name is :when:

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

After this, Test.neg/1 works:

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