Macro to create module attributes?

I am trying to define a macro like this:

defmodule MyModule do
  defmacrop def_sort_order(name, order) do
    quote do
      Module.put_attribute(
        __MODULE__,
        unquote(name),
        Enum.with_index(unquote(order)) |> Map.new()
      )
    end
  end

  def_sort_order(
    :test_order,
    [
      :a,
      :b,
      :c
    ]
  )
end

And then later in the module I would like to access the attribute in some function and it should look like this:

IO.inspect(@test_order)
  %{a: 0, b: 1, c: 2}

But I keep getting undefined function def_sort_order/2 (there is no such import)
What I am doing wrong here?

You can’t call macros defined in a module in the same module’s body, it has to be within a def:

defmodule Foo do
  defmacrop foo, do: (quote(do: "foo"))

  def do_foo, do: foo()
end

To do what you’re looking to do, you would have to define your macro in another module (as defmacro) and require or import it in the modules you want to use it in. This is because you’re trying to call def_sort_order on compilation, but the module hasn’t been compiled yet.

3 Likes

A simple anonymous function declared and used at compile time would also work:

defmodule MyModule do
  def_sort_order = fn name, order ->
    Module.put_attribute(
      __MODULE__,
      name,
      Enum.with_index(order) |> Map.new()
    )
  end

  def_sort_order.(:test_order, [:a, :b, :c])
end

Although in that case, you might not even need any macro or function especially to call Module.put_attribute/3, this could just be simple module attributes:

defmodule MyModule do
  @test_order Enum.with_index([:a, :b, :c]) |> Map.new()
end

or just a fn to generate the attribute:

defmodule MyModule do
  sort_order = fn order -> Enum.with_index(order) |> Map.new() end

  @test_order sort_order.([:a, :b, :c])
end
4 Likes