Creating a variable with a macro, and then use it in its body

I’m trying to do something similar to wath ecto does when defining schemas. Basically the idea is:

defmodule Test do
  use TableBuilder

  row "test" do
    field "field1"
    field "field2"
  end
end

row and field are macros defined in the TableBuilder module, what I’m trying to achieve is a DSL that underneath builds a Row struct, that has a fields collection of Field structs

here is what I’m doing right now:

defmodule TableBuilder do
  defmacro __using__(_opts) do
    quote do
      import unquote(__MODULE__)
      Module.register_attribute __MODULE__, :rows, accumulate: true
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def get_table do
        inspect @rows
      end
    end
  end

  defmacro row(name, do: fields) do
    row = %Row{name: name}
    quote do
      @rows {unquote(row), unquote(fields)}
    end
  end

  defmacro field(name) do
    quote do
      %Field{name: unquote(name)}
    end
  end
end

the problem is:

when the field macro is called, I need to access the row variable created in the row macro.
But I can’t find a solution.

What’s the problem with this approach? Am I doing it wrong? Do you see better approaches to the problem?

Many thanks in advance!

4 Likes

By default, Macros in Elixir are unable to access variables outside of their definition. This is called ‘hygienic’. It is also possible to create unhygienic macros, which are what you are looking for here. Saša Jurić’s guide on Macros has some great information on this subject.

3 Likes

thanks for pointing me there…after many attempts I found a nice solution. Not so clean, but it’s working. :smiley:

1 Like

I looked through the Schema source code of Ecto yesterday and they seem to use Module.set-attribute/1 and Module.get_attribute/1 to ‘cheat’ the variable-passing at compile-time. Roughly the following happens:

  1. the schema macro is called.
  2. amongst other things, astruct_fields module attribute with append: trueis instantiated.
  3. the do block passed to the macro is evaluated.
  4. each field, has_one, has_manyadds the specified field to the struct_fieldsmodule attribute
  5. back after the do-evaluation, the struct_fields are read and a struct is defined with them.
1 Like

yes! And this is exactly what I’m doing right now.

But this approach do not work perfectly for me for two reasons:

  • I need to define multiple rows in a single module (and right now I’m using the fields attribute and then wipe it right after the macro body invocation to be empty for the next row)

  • the fact that you are not forced in any way to use field inside a row macro, so weird things could happen. But maybe this is also a problem for ecto…and with a good documentation should be ok.

I’d really love to create a struct in the row macro and than pass it (like var!) when unquoting the body, but as far as I know it’s impossible…

2 Likes