abitdodgy

abitdodgy

How to inject macro functions at end of module

I’m using the __using__ macro to define some boilerplate code. One of the functions defined in the macro is a different arity of a function defined in the module that uses the macro. Here’s an example:

defmodule Obramax.ChannelsFactory do
  use Obramax.FactoryUtils
  alias Obramax.Channels.Channel

  def build(:channel) do
    %Channel{}
    |> struct(attrs_for(:channel))
  end
end

defmodule Obramax.FactoryUtils do
  defmacro __using__(_) do
    quote do
      def build(factory_name, attributes \\ []) do
        factory_name
        |> build()
        |> struct(attributes)
      end
    end
  end
end

I can’t get this to work because the macro will inject build/2 into ChannelsFactory before it has defined build/1, this throwing a CompileError: ** (CompileError) test/support/factories/channels_factory.ex:14: def build/1 conflicts with defaults from build/2

If I move use FactoryUtils to the bottom of ChannelsFactory I can get it to work, but that seems strange and might the code ambiguous.

Are there other solutions?

Marked As Solved

abitdodgy

abitdodgy

Thanks for the tip. I saw the compilation hooks. I changed FactoryUtils to use @before_compile.

defmodule Obramax.FactoryUtils do
  defmacro __using__(_) do
    quote do
      @before_compile Obramax.FactoryUtils
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def build(factory_name, attributes \\ []) do
        factory_name
        |> build()
        |> struct(attributes)
      end
    end
  end
end

And I didn’t have to change anything in ChannelsFactory.

defmodule Obramax.ChannelsFactory do
  use Obramax.FactoryUtils
  alias Obramax.Channels.Channel

  def build(:channel) do
    %Channel{}
    |> struct(attrs_for(:channel))
  end
end

Not sure if this is the best way, but it works.

Also Liked

Eiji

Eiji

I believe that you should add also @behaviour attribute into __using__ macro, so code would look cleaner. For now somebody reading __before_compile__ would not know from where build/1 comes and with behaviour you could require it.

Eiji

Eiji

The problem is that you are already defining build/1 without noticing that! :077:

Look that when you write such code in iex:

defmodule Example do
  def sample(first_arg, second_arg \\ nil), do: {first_arg, second_arg}
end

and after it you wrote Example. + hit <Tab> (for printing suggestions) then you would see:

> Example.sample
sample/1    sample/2

Which means you have defined sample/1 as well as sample/2 by just one function definition (using default argument). It’s working like:

defmodule Example do
  def sample(first_arg), do: sample(first_arg, nil)
  def sample(first_arg, second_arg), do: {first_arg, second_arg}
end

So your problem is build/1 function definition inside __before_compile__ - it’s why compiler does not show you any warning. Notice that when you remove default value you should see:

warning: function build/1 required by behaviour Obramax.Factory is not implemented (in module Obramax.ChannelsFactory)

Eiji

Eiji

Yeah or … just rename one function. :smiley:

For example in __before_compile__ you can have full_build/1 and full_build/2 and in behaviour you would have only build/1.

Also notice that build/1 from __before_compile__ conflicts with build/1 from behaviour, so in fact one of them (if I understand it correctly build/1 from behaviour) is never used.

Eiji

Eiji

You can find all you need in Module docs: Compile callbacks.

abitdodgy

abitdodgy

Thanks for clarifying that. I put build/1 in __before_compile__ to throw an error instead of letting the process hang when the function call can’t pattern match. For example, if you use an atom that has no function definition: build(:i_dont_exist)).

And now I know why it hangs. When a build/1 call can’t pattern match, it falls through to build/2. But in reality, it’s falling through to build/1 that’s automatically expanded due to the default argument, which again calls build/2 with the unknown atom as the first argument, and a list as the second argument: build(:i_dont_exist, []).

Then build/2 calls build/1 again (the expanded one, from the default argument):

def build(factory_name, attributes) do
  factory_name
  |> build() # <-- here
  |> struct(attributes)
end

… and the process keeps repeating.

Thanks! This whole thread has been illuminating. I learned a bunch of stuff today!

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

Tee
can someone please explain to me how Enum.reduce works with maps
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement