0
votes

I'm learning Elixir and experimenting with the Macro system.
I am trying to make a variable available to a block that will be wrapped with other code by a macro, but I am clearly doing something wrong.

This is my starting point macro, which works. I am ware that there are more idiomatic ways to iterate in Elixir, and I wrote it just as an exercise:

defmodule Loops do
  defmacro times(n, [do: block]) do
    quote do
      Enum.each(1..unquote(n), fn(_) -> unquote(block) end)
    end
  end
end

import Loops

times 3 do
  IO.puts "Hello!"
end

Now, what I want to accomplish is being able to reference the counter from the do block. So, if I give a proper name to the fn argument:

Enum.each(1..unquote(n), fn(_) -> unquote(block) end)
# becomes:
Enum.each(1..unquote(n), fn(counter) -> unquote(block) end)

I would like to be able to do something like this:

times 3 do
  IO.puts "Hello! iteration n: #{counter}"
end

This however doesn't work and raises a CompileError because of undefined function counter/0. The error is raised from the do block I passed to the macro invocation, which I find a bit counterintuitive because I thought that the block would be placed in the expanded code when calling unquote().

Am I approaching the problem in the wrong way, or is this simply not possible?

1

1 Answers

3
votes

You can use Kernel.var!/2 to make counter an "unhygienic" variable. This will make sure it is available to the generated code without being renamed by Elixir's macro system.

defmodule Loops do
  defmacro times(n, [do: block]) do
    quote do
      Enum.each(1..unquote(n), fn(var!(counter)) ->
        unquote(block)
      end)
    end
  end
end

defmodule Main do
  require Loops

  def main do
    Loops.times 3 do
      IO.puts "Hello! iteration n: #{counter}"
    end
  end
end

Main.main

Output:

Hello! iteration n: 1
Hello! iteration n: 2
Hello! iteration n: 3