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
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
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
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)
Eiji
Yeah or … just rename one function. ![]()
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.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









