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
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.
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.
** (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.
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).
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