Define multiple modules in macro, only last one gets created

I have a macro that serves as a wrapper around the definition of two modules.

defmacro transformers_for(fields) do
  quote do
     defmodule Incoming do
  end
  for field <- unquote(fields) do
     #....define incoming module 
  end
  quote do
     end
  end

  quote do
     defmodule Outgoing do
  end
  for field <- unquote(fields) do
     #....define outgoing module 
  end
  quote do
     end
  end
end

I am fairly new to Elixir macros, not entirely sure if this is the correct way, but it compiles. However, only the final (Outgoing) module gets defined after expanding the macro and Incoming never seems to materialise. Is this expected behaviour? How can I fix this?

Macros are like functions in the sense that they receive data and must return data. In other words, every quote expression you define MUST be returned. If you return only the last quote, that’s the only quote seen by the caller and the only quote that will be expanded. Try putting every quote in a variable and then returning a list of quotes at the end.

Also I would be careful with defining multiple modules with a single macro. I think in all of those years writing Elixir code, I remember only once writing a macro that defines multiple modules and the number of modules were 2. There is likely a solution out there that relies less on code generation.

Thanks, that makes sense. But if that is the case, why does my code correctly define the final module? Shouldn’t it error out because only a quoted end gets returned?

I immediately accept your assumption there is a different solution, and I did look for it, but haven’t found it. To give you a bit of context, I am building an API gateway (based on Rackla) that needs to do some transformations of data passing trough in each direction. I am using a presenter module called Remodel (stavro/remodel) which necessitates the creation of separate modules for each direction.

Thanks, that makes sense. But if that is the case, why does my code correctly define the final module?

Because the final quote is the only one that you return. Imagine you have a function where you want to return the numbers 1, 2 and 3. If you write it as:

def numbers do
  1
  2
  3
end

The function above will return only 3 since that’s the last value. You can fix it by returning a list with all the numbers or by assigning every number to a variable and returning all variables:

def numbers do
  one = 1
  two = 2
  three = 3
  [one, two, three]
end

That’s the change you should do to your quotes too.

To give you a bit of context, I am building an API gateway (based on Rackla) that needs to do some transformations of data passing trough in each direction.

I don’t have enough context about Rackla nor your current issue but, if you can generate your incoming and outgoing modules, it also means you can get them to behave as you want by passing the data you need as parameter. For example, imagine you have the modules A, B and C. Instead of generating A.Incoming, A.outgoing, B.incoming, B.outgoing, and so on, you can likely have something such as {Incoming, A}, {Outgoing, A}, {Incoming, B}, …, where the extra information you need is in the module you pass as data to the incoming/outgoing transformers. This way, instead of defining m * n modules, you will need only m + n.

2 Likes

I understand your suggestion, but if you look closely at my originally posted code, the thing that the macro returns is:

quote do 
    end
end

I don’t do any clever concatenation of quotes there, yet it still picks up the stuff above it somehow? Anyway, I have it working now, will look into your further suggestions and keep learning.

And may I just take this opportunity to let you how much I admire and appreciate the thing you have done with Elixir? Unlock all that potential for us mere mortals, thank you so very much for showing us the way and giving us a programming language that can give you actual goosebumps.

1 Like

Oh, I see what you mean. This code you posted:

  quote do
     defmodule Outgoing do
  end
  for field <- unquote(fields) do
     #....define outgoing module 
  end
  quote do
     end
  end

It is actually being parsed as:

  quote do
    defmodule Outgoing do
    end
    for field <- unquote(fields) do
       #....define outgoing module 
    end
    quote do
    end
  end

That should explain the confusion!

And may I just take this opportunity to let you how much I admire and appreciate the thing you have done with Elixir?

Thank you!

Ah yes, bit of a face palm there :flushed: For posterity, here’s my actually working solution:

defmodule Transform do
  
  defmacro transformer(name, fields) do
    quote do
      defmodule unquote(name) do
        use Remodel
        unquote do
          for {name, as} <- fields do
            quote do
              attribute unquote(name), as: unquote(as)
            end
          end
        end
      end
    end
  end

  defmacro transform(fields) do
    reversed = Enum.map(fields, fn {k, v}-> {v, k} end)
    quote do
      transformer(Incoming, unquote(fields))
      transformer(Outgoing, unquote(reversed))
    end
  end
end

Any suggestions for further refinement are most welcome of course.