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.
Last Post!
Eiji
You can’t expand them, because unlike sigil they are special forms. However if you still expect to pass raw map then you can solve that in few ways depends on use case …
defmodule UsedModule do
defmacro __using__(opts) do
if get_config!(opts, __CALLER__).with_extra do
quote do
def extra(), do: "with extra"
end
else
quote do
def extra(), do: "no extra"
end
end
end
defp get_config!(opts, env) do
case opts[:config] do
nil ->
raise ":config option not passed"
{:%, _, [{:__aliases__, _, [:PatternMatchingConfig]}, {:%{}, _, config}]} ->
struct(PatternMatchingConfig, config)
{:%, _, [aliases, {:%{}, _, config}]} ->
aliases |> Macro.expand_literal(env) |> struct(config)
{:%{}, _, config} ->
Map.new(config)
_config ->
raise "unexpected config value. Did you passed a raw map?"
end
end
end
defmodule ExpandedConfig do
defstruct with_extra: false
end
defmodule PatternMatchingConfig do
defstruct with_extra: false
end
defmodule Examples do
defmodule ExpandedStruct do
use UsedModule, config: %ExpandedConfig{}
end
defmodule PatternMatchingMap do
use UsedModule, config: %{with_extra: true}
end
defmodule PatternMatchingStruct do
use UsedModule, config: %PatternMatchingConfig{}
end
end
If you do not want to mix your code with handling special forms then all you need to do is write something like:
defmodule Example do
def sample(ast, env) do
if Macro.quoted_literal?(ast) do
Macro.postwalk(ast, fn
{:__aliases__, _, _} = alias -> Macro.expand(alias, env)
{:%{}, [], opts} -> Map.new(opts)
{:%, [], [module, opts]} -> struct(module, opts)
other -> other
end)
else
ast
end
end
end
iex> map_ast = quote do: %{day: 1, month: 1, year: 2022}
{:%{}, [], [day: 1, month: 1, year: 2022]}
iex> Example.sample(map_ast, __ENV__)
%{day: 1, month: 1, year: 2022}
iex> struct_ast = quote do: %Date{day: 1, month: 1, year: 2022}
{:%, [],
[
{:__aliases__, [alias: false], [:Date]},
{:%{}, [], [day: 1, month: 1, year: 2022]}
]}
iex> Example.sample(struct_ast, __ENV__)
~D[2022-01-01]
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
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









