mruoss
__using__ macro with option
Hi community!
What is the correct way to implement the __using__ macro with an option that changes the way generated functions behave? Or is that a no-go by itself?
An example:
defmodule UsedModule do
defmacro __using__(opts) do
quote do
if Keyword.get(unquote(opts), :with_extra, false) do
def extra(), do: "with extra"
else
def extra(), do: "no extra"
end
end
end
end
defmodule UsingModuleWithExtra, do: use UsedModule, with_extra: true
defmodule UsingModuleNoExtra, do: use UsedModule
iex> UsingModuleWithExtra.extra()
"with extra"
iex> UsingModuleNoExtra.extra()
"no extra"
My question: Is this the way to go inside the __using__ macro? Or would you define the extra function once and do the if inside like this?
def extra() do
if Keyword.get(unquote(opts), :with_extra, false),
do: "with extra",
else: "no extra"
end
Or is there a cleaner way I’m not seeing?
Thanks!
Marked As Solved
Eiji
In case you have a static output like in this example then you can simply call unquote(string) and outside quote block set string variable based on option.
defmodule UsedModule do
defmacro __using__(opts) do
string = if opts[:with_extra], do: "with extra", else: "no extra"
quote do
def extra(), do: unquote(string)
end
end
end
In other case I would recommend to simply move if condition outside quote block and simply use 2 quote calls.
defmodule UsedModule do
defmacro __using__(opts) do
if opts[:with_extra] do
quote do
def extra(), do: "with extra"
end
else
quote do
def extra(), do: "no extra"
end
end
end
end
Note: If opts[:with_extra] does not exists then you got nil which is falsy value and therefore we do not need a Keyword.get/3 call here. ![]()
Also Liked
Eiji
Yeah, I can say that it may depend on case. For example there is a big difference working on raw data passed to macro and variables passed to macro even if their declaration was static one line above …
some_macro("some value")
# param value in macro: "some value"
value = "some value"
some_macro(value)
# param value in macro: {:value, […], nil}
unquote inside quote block solves the problem. However in some cases you may expect some value to be passed raw instead.
some_value = true
quote do
if unquote(some_value) do
:ok
else
:error
end
end
Here since the value is known “outside” the half of code we generate would never be used.
A simple example shows how important it may be:
defmodule MyMacro do
defmacro __using__(opts) do
Enum.map(opts[:enabled_features] || [], &apply_feature/1)
end
defp apply_feature(:feature_name) do
quote do
# …
end
end
# …
end
Look that if we would have only 1/10 features enabled then we would generate only one quote block.
However if we would write it like this:
defmodule MyMacro do
@all_features [:feature_name, # …]
defmacro __using__(opts) do
quote do
import MyMacro, only: [apply_feature: 1]
for feature <- unquote(@all_features), feature in unquote(opts[:features] || []) do
apply_feature(feature)
end
end
end
defmacro apply_feature(:feature_name) do
quote do
# …
end
end
# …
end
then said module with one enabled feature would have generated extra loop with 9/10 skipped elements. Also in example above apply_feature/1 is a public macro for absolutely no reason (comparing to previous example).
Also it would be nice to provide a link for mentioned text as José could talk about other case or just more complex example. It would be good for others to see all sides of this coin. ![]()
Eiji
If it’s stupid, but it works then it’s no stupid! ![]()
I have used atom as it’s best for what I called feature_name as those in apply_feature/1 are known at compile-time. You can use strings, integers and every other data that you can write pattern-matching for.
Also take a look at example below:
some_macro(["a", "b", "c"])
# param value in macro
# ["a", "b", "c"]
some_macro(~w[a b c])
# param value in macro
# {:sigil_w, [delimiter: "[", context: Elixir, imports: [{2, Kernel}]],
# [{:<<>>, [], ["a b c"]}, []]}
some_macro(~w[a b c])
# value of `Macro.expand(param, __CALLER__)`
# ["a b c"]
If you want to call String.to_atom/1 inside macro then:
- You can call
Enum.map(list, &String.to_atom/1)as long as you require list of atoms passed as raw list or sigil (see above example) - In function definition
String.to_atom/1call needs to be passed insideunquote/1call. - Inside
moduleandfunctionblock you can useString.to_atom/1call as well insideunquote/1call as outsideunquote/1call.
Hope that helps.








