2
votes

Continuing the Binding and unquote fragments example from the Elixir documentation...

We have a macro that defines functions based on a Keyword list.

defmodule MacroFun do

  defmacro defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

end

defmodule Runner do
  require MacroFun

  kv = [foo: 1, bar: 2]
  MacroFun.defkv(kv)

end

Runner.foo

Now let's move the body of the macro to a helper function.

  defmacro defkv(kv) do
    _defkv(kv)
  end

  defp _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

Great, everything still works. But now what if we want to make another macro that modifies kv before passing it to the private helper function:

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      _defkv(modified_kv)
    end
  end

That doesn't work. Elixir says _devkv is not defined. We can fix by using a fully qualified function name:

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      MacroFun._defkv(modified_kv)
    end
  end

But then Elixir complains that MacroFun._defkv is private. So we change it to public, but it still doesn't work because the helper method _devkv returns quoted code to our macro def_modified_kv which itself is quoted!

So we can fix that by eval'ing the code returned by the helper function (final code):

defmodule MacroFun do

  defmacro defkv(kv) do
    _defkv(kv)
  end

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      MacroFun._defkv(modified_kv) |> Code.eval_quoted([], __ENV__) |> elem(0)
    end
  end

  def _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

end
  1. Why did I have to change to calling the helper function by its fully qualified name?
  2. Why did I have to change the helper function to be public (from private)?
  3. Is there a better way to do this besides calling Code.eval_quoted?

I feel like I'm doing something wrong.

Thanks for the help.

2

2 Answers

0
votes
  1. You need to import the module.
  2. When you import a module, you can only import public methods.

  3. Macros are a compile time feature, they write actually write code (or AST) in the module. So, the context of the macro when it's expanded, is where you're calling/using it. Thus, if you use a macro (not defined in) SomeOtherModule that's the context of macro expansion. In SomeOtherModule you would need to import MacroFun to call its functions locally.

The code below works in Elixir >= 1.2.0

E.g.

defmodule MacroFun do

  defmacro defkv(kv) do
    _defkv(kv)
  end

  defp _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      defkv(modified_kv)
    end
  end
end

defmodule Runner do
  import MacroFun

  kv = [foo: 1, bar: 2]
  def_modified_kv(kv)

end

Hope this answers your questions! Good luck.

Updated a couple times because macros are a little confusing!

0
votes

1) By using require, you've told the compiler to compile and make available MacroFuns public macros and functions. Note that the functions would be available anyway though:

Notice that usually modules should not be required before usage, the only exception is if you want to use the macros from a module.

2) They are available through their FQN

3) Here's my take on it:

defmodule MicroFun do
  defmacro defkv(kv) do
    _defkv(kv)
  end

  defp _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k,v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

  defmacro modifier(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.map kv, fn {k,v} -> {k, v+1} end
    end
  end
end

defmodule Runner do
  require MicroFun

  [foo: 1, bar: 2]
  |> MicroFun.modifier
  |> MicroFun.defkv
end

The idea is that you don't need to nest macros/functions when you can just pipe them.