3
votes

It's common to have constants as Module Attributes in Elixir. I've tried to pass module attributes as arguments to different macros from different libraries (that usually define a new module):

defmodule X do
  @data_types [:x, :y, :z]
  @another_constant "some-constant-value"

  defenum DataType, :type, @data_types
end

Here's another example of passing module attributes as an argument to a Macro


But almost always get an error along the same lines:

** (Protocol.UndefinedError) protocol Enumerable not implemented for {:@, [line: 26], [{:types, [line: 26], nil}]}

So I usually end up repeating the values:

defmodule X do
  @data_types [:x, :y, :z]
  @another_constant "some-constant-value"

  defenum DataType, :type, [:x, :y, :z]
end

I know repeating them most of the time isn't usually a big deal, but I would really like to know how would I be able to pass the value of a module attribute to a macro.

This is especially apparent in macros that define new modules (like Amnesia and EctoEnum).


So far I've tried a bunch of things, including:

  • Expanding the value using the Macro module
  • Evaluating the value using Code module
  • Fetching the value using Module.get_attribute/2
  • Trying different variations of quote/unquote calls

But nothing has worked. I have a feeling the macro needs to be written in a way that it can read them. If so, how should the macro be written for it to work?

2

2 Answers

3
votes

Unfortunately, the only way to fix the issue with passing arbitrary quoted expressions to external libraries would be to provide pull requests fixing issues in the libraries.

Consider the following example

defmodule Macros do
  defmacro good(param) do
    IO.inspect(param, label: "đź‘Ť Passed")
    expanded = Macro.expand(param, __CALLER__)
    IO.inspect(expanded, label: "đź‘Ť Expanded")
  end

  defmacro bad(param) do
    IO.inspect(param, label: "đź‘Ž Not Expanded")
  end
end

defmodule Test do
  import Macros
  @data_types [:x, :y, :z]

  def test do
    good(@data_types)
    bad(@data_types)
  end
end

The declaration of Test prints:

đź‘Ť Passed: {:@, [line: 28], [{:data_types, [line: 28], nil}]}
đź‘Ť Expanded: [:x, :y, :z]
đź‘Ž Not Expanded: {:@, [line: 29], [{:data_types, [line: 29], nil}]}

If the 3rd-party library does not call Macro.expand/2 on the argument, the quoted expressions won’t be expanded. Below is the excerpt from the documentation:

The following contents are expanded:

• Macros (local or remote)
• Aliases are expanded (if possible) and return atoms
• Compilation environment macros (__CALLER__/0, __DIR__/0, __ENV__/0 and __MODULE__/0)
• Module attributes reader (@foo)

That said, to have the ability to accept module attributes or macro calls like sigils, the 3rd party library macros must call Macro.expand on arguments. You cannot fix this issue from your client code.

0
votes

But almost always get an error...

I'm able to access a module attribute inside a macro in this example:

defmodule My do
  @data_types [:x, :y, :z]

  defmacro go() do
    quote do
      def types() do
        unquote(@data_types)
      end
    end
  end
end

defmodule Test do
  require My
  My.go()

  def start do
    types()
  end
end

In iex:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Test.start()
[:x, :y, :z]

iex(2)> 

Response to comment:

Here's an example that passes a module attribute as an argument to a macro function call:

defmodule My do

  defmacro go(arg) do
    quote do
      def show do
        Enum.each(unquote(arg), fn x -> IO.inspect x end)
      end
    end
  end

end

defmodule Test do
  @data_types [:x, :y, :z]

  require My
  My.go(@data_types)

  def start do
    show()
  end

end

In iex:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Test.start()
:x
:y
:z
:ok

iex(2)> 

If instead, you try this:

defmodule My do
  defmacro go(arg) do
    Enum.each(arg, fn x -> IO.inspect x end)
  end

end

defmodule Test do
  require My
  @data_types [:x, :y, :z]
  My.go(@data_types)

  def start do
    show()
  end
end

then in iex you'll get an error:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (Protocol.UndefinedError) protocol Enumerable not implemented for {:@, [line: 11], [{:data_types, [line: 11], nil}]}
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:141: Enumerable.reduce/3
    (elixir) lib/enum.ex:1919: Enum.each/2
    expanding macro: My.go/1
    my.exs:11: Test (module)

That error occurs because you are trying to enumerate an ast:

{:@, [line: 11], [{:data_types, [line: 11], nil}]}

which is not a list (or any other enumerable--it's obviously a tuple!). Remember that arguments to macros are ast's.

The reason this works:

  defmacro go(arg) do

    quote do
      def show do
        Enum.each(unquote(arg), fn x -> IO.inspect x end)
      end
    end

  end

is because quote() creates an ast, and unquote(arg) injects some other ast into the middle of the ast. The macro call My.go(@data_types) executes at compile time, and elixir replaces the macro call with the ast returned by My.go(), which is a function definition named show().