1
votes

I am playing around with Elixir macros - specifically macros that call themselves which is something that I do often in Scheme. I have created a little test macro below however it just hangs iex - nothing is printed to console. Does anyone have any insight into why and what could be done to correct it?

defmodule RecMac do
  defmacro test_rec(x) do
    quote do
      IO.puts("Started?")
      if(unquote(x) < 1) do
        IO.puts("Done?")
        "done"
      else
        IO.puts("Where are we")
        IO.puts(unquote(x))
        RecMac.test_rec(unquote(x) - 1)
      end
    end
  end
end

EDIT!!

OK, so it turns out you can define recursive macros where there is a structural difference to match on (eg lists). The following is working for me. And to confirm @Aleksei Matiushkin below, the above will not work and does indeed not work in scheme!

  defmacro test_rec([h | t]) do
    quote do
      IO.inspect([unquote(h) | unquote(t)])
      RecMac.test_rec(unquote(t))
    end
  end

  defmacro test_rec([]) do
    quote do
      IO.puts "Done"
    end
  end
end

I am very happy to have dug into this as I learned something about two languages!

2

2 Answers

3
votes

TL;DR: this is impossible.


Macros in are not what you expect them to be. When a compiler sees a macro, it calls it during a compilation time and injects the AST it returned in the place of where it was called. That said, recursive macros would always lead to the infinite loop at the compilation stage.

Put IO.puts("something") before quote do instruction and you’ll see it to be printed infinitely, once per subsequent call to expand the macro.

You can use @compile {:inline, test_rec: 1} to achieve the behaviour you are after. For better understanding macros, you probably should read Macros section in the Elixir Guide in general and this excerpt in particular:

Macros are harder to write than ordinary Elixir functions and it’s considered to be bad style to use them when they’re not necessary. So write macros responsibly.

3
votes

Actually you can do kind of recursion, but point is to think what you are doing to avoid calling macro inside quote do [....] end. Your example with IO.puts would be something like this

defmodule RecMac  do
  defmacro test_rec(list) do
    {:__block__, [], quote_value(list)}
  end

  defp quote_value([]), do: []
  defp quote_value([h | t]) do
    if h < 1 do
      []
    else
      ast = quote do
         IO.puts("#{unquote(h)}")
      end
      [ast | quote_value(t)]
    end
  end
end

you will notice two things, I do use recursion in macro but outside quote using private function quote_value/1 and that function has logic that "stops" after it finds value lower than 1. All "quotes" are put into list and trick is to put this list into tuple {:__block__, [], put_quote_list_here}

Now note that this macro wont compile if list parameter of test_rec is not know upfront (during compile time), so you need to call macro test_rec(["a", "b", 0, 100, 200]) so compiler knows size and elements of that list.

BTW, I used body optimized recursion, but you can easily add accumulator and convert this into tail optimized recursion.