DRY up macro "use" opts

Say I have some macros that take in the exact same __using__ opts like so:

defmodule MyMacroA do
  defmacro __using__(opts) do
    _foo = Keyword.fetch!(opts, :foo)
  end
end

defmodule MyMacroB do
  defmacro __using__(opts) do
    _foo = Keyword.fetch!(opts, :foo)
  end
end

defmodule UseMyMacro1 do
  use MyMacroA, foo: "bar"
end

defmodule UseMyMacro2 do
  use MyMacroB, foo: "bar"
end

How do I DRY up the opts? I tried the following which doesn’t work:

defmodule MyMacroConfig do
  def config, do: [foo: "bar"]
end

defmodule UseMyMacro3 do
  use MyMacroA, MyMacroConfig.config()
end

Hi @bobics,

You have already discovered that the configs are repeated, so you can also define them as a constant inside your defmacro and then override it from the caller, rather than dealing with config function.

Macros are good, but it can also complicate your code. So, whenever possible, please refrain from using them, instead go for simple modules.

Thanks.

1 Like

If I initialize the opts with default attributes, I still have to do it for both MyMacroA and MyMacroB, which doesn’t DRY anything up. Also, as there can be only one default, it quickly becomes limiting.

Let’s assume my use case for macros is valid. It seems you’re saying there isn’t a way to do what I want.

What error message do you see when you try it? This always helps us to debug your problem.

But my assumption is, that elixir yells at you because the AST of MyMacroConfig.config() were not a keyword-list.

You need to actually call that function first by evaluating it.

Code.eval_quoted/3 can help you.

3 Likes

Thanks @NobbZ, the error message I’m seeing is:

** (FunctionClauseError) no function clause matching in Keyword.fetch!/2    
    
    The following arguments were given to Keyword.fetch!/2:
    
        # 1
        {{:., [line: 26], [{:__aliases__, [line: 26, counter: -576460752303423294], [:MyMacroConfig]}, :config]}, [line: 26], []}
    
        # 2
        :foo

I tried your suggested an it worked! I don’t understand why Code.eval_quoted/3 returns a tuple though.

defmodule MyMacroA do
  defmacro __using__(opts) do
    # why is this a tuple?: `{[foo: "bar"], []}`
    {evald_opts, _} = Code.eval_quoted(opts)

    _foo = Keyword.fetch!(evald_opts, :foo)
  end
end

ALSO, reading the Elixir docs on Eval.quoted/3 give me a second though on using it. I’ll basically need to weigh whether DRY’ing is worth it. At first glance it seems “safe” enough since MyMacroConfig.config/0 just returns a keyword list. The docs:

Warning : Calling this function inside a macro is considered bad practice as it will attempt to evaluate runtime values at compile time.

The second argument returns a keyword-list of ‘bindings’, which are variables whose values are set (or altered) by the evaluation of the quoted snippet. Usually you do not care about these (because we almost always want our macros to be hygienic) but in some special cases you’d want to extract the values returned in there.

2 Likes

Correct! If it is possible for your use-case to delay the fetching of the configuration arguments until runtime, this is always better (because it for instance allows the configuration to be dynamically changed, and allows it to be based on things only known at runtime).

2 Likes

Thanks, totally makes sense. In my particular use case, I’m defining functions in the quote block, and all the configuration is constant, typically module names. e.g.

quote do
 # where `a_struct_module` is passed in via opts
  def my_fun(%unquote(a_struct_module){} = a) ...
end