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.
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
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.
I’m don’t think that helps, unless I’m doing it wrong. I added the behaviour, but I don’t see any compiler warnings when defining modules without build/1.
defmodule Obramax.Factory do
@callback build(factory_name :: atom) :: struct()
defmacro __using__(_) do
quote do
@before_compile Obramax.Factory
@behaviour Obramax.Factory
end
end
defmacro __before_compile__(_env) do
quote do
alias Obramax.Repo
def build(factory_name, attributes \\ []) do
factory_name
|> build()
|> struct(attributes)
end
end
end
end
The factory definition is unchanged (but, for my test, I created a factory without a build/1 to see what the compiler did):
defmodule Obramax.ChannelsFactory do
use Obramax.Factory
alias Obramax.Channels.Channel
def build(:channel) do
%Channel{}
|> struct(attrs_for(:channel))
end
end
Presumably, if I define a factory without build/1 I should see a compiler warning, right? That doesn’t happen. I think this is because build/2 with a default argument defined in __before__compile expands to build/1.
Due to the way I defined my functions, if you call build/1 on a factory without having defined it, it falls through to build/2 defined in __before_compile__, which hangs the process.
Maybe it’s better to define a function that raises an error? For example:
defmodule Obramax.Factory do
defmacro __before_compile__(_env) do
quote do
# ...
def build(factory_name) do
raise RuntimeError, "Factory `#{factory_name}` not defined."
end
def build(factory_name, attributes \\ []) do
factory_name
|> build()
|> struct(attributes)
end
end
end
end
The problem is that you are already defining build/1 without noticing that!
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)
Right, I thought that was happening. But I struck out the text thinking I was wrong. So in this case a behaviour wouldn’t really help, unless I remove the default value from the second argument and make it required…
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.
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!