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

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
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
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

We're in Beta

About us Mission Statement