I have a map that I want to use a single source of truth for a couple of functions. Let's say it is:
source_of_truth = %{a: 10, b: 20}
I'd like the keys of that map to be values of EctoEnum. EctoEnum provides a macro defenum that I should use like this:
defenum(
EnumModule,
:enum_name,
[:a, :b]
)
I don't want to repeat [:a, :b] part. I'd like to use the keys from the map instead like this:
defenum(
EnumModule,
:enum_name,
Map.keys(source_of_truth)
)
It doesn't work because defenum macro expects a plain list.
I thought I could do it by defining my own macro like this:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(enum_values)
)
end
end
and then call:
dynamic_enum(EnumModule, :enum_name, Map.keys(source_of_truth))
However, it doest the same thing: enum_values is not a precomputed list but AST for Map.get. My next approach was:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
values = unquote(enum_values)
defenum(
unquote(enum_module),
unquote(enum_name),
?
)
end
end
Not sure what I could put where the ? is. I can't just put values because it is a variable and not a list. I can't put unquote(values) either.
A solution that sort of works is this one:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
{values, _} = Code.eval_quoted(enum_values)
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(values)
)
end
end
However, the docs say that using eval_quoted inside a macro is a bad practice.
[EDIT]
A solution with Macro.expand does not work either because it doesn't actually evaluate anything. The expansion stops at:
Expanded: {{:., [],
[
{:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
:get_attribute
]}, [],
[
{:__MODULE__, [counter: -576460752303357631], Kernel},
:keys,
[
{:{}, [],
[
TestModule,
:__MODULE__,
0,
[
file: '...',
line: 16
]
]}
]
]}
So it does not expand to the list as we expected.
[\EDIT]
What is a good solution for that problem?
As stated in the documentation for Macro.expand/2
The following contents are expanded:
Macros (local or remote)
Aliases are expanded (if possible) and return atoms
Compilation environment macros (__CALLER__/0, __DIR__/0, __ENV__/0 and __MODULE__/0)
Module attributes reader (#foo)
Emphasis is mine. So the possibility would be to use module attributes with Macro.expand/2.
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
And call it like:
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
FWIW, the full code:
$ \cat lib/eenum.ex
defmodule Eenum do
import EctoEnum
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
end
$ \cat lib/tester.ex
defmodule Tester do
import Eenum
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
end
FWIW 2. To be able to call dynamic_enum as shown above from the module scope, all you need is (surprise :) another module scope, already compiled at the moment of macro invocation:
defmodule Defs do
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
defmacro keys, do: Macro.expand(#keys, __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
FWIW 3. The latter (explicit module with definitions) will work even without a necessity to have module attributes:
defmodule Defs do
defmacro keys, do: Macro.expand(Map.keys(%{a: 10, b: 20}), __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
The rule of thumb is when you find yourself in a need to invoke Code.eval_quoted/3, put this code into the independent module and make compiler invoke this code compilation for you. For functions is works on the module level, for module level it should be put into another module to make module context (aka __CALLER__ and __ENV__) available.
I battled with the same problem a while back. Basically you can build your syntax tree in a quote, using unquote to inject your dynamic value, and then use Code.eval_quoted to eval the macros:
options = Map.keys(source_of_truth)
Code.eval_quoted(
quote do
EctoEnum.defenum(MyEnum, :type_name, unquote(options))
end,
[],
__ENV__
)
I have the following definition of my macro
defmacro defverified(sign, body) do
{name, _, [param]} = sign
quote do
def unquote(name)(unquote(param)) do
unquote(param) = verify! param
unquote(body)
end
end
end
Were verify!/1 returns its paramters if it is verified as a correct parameter
And my function is defined as follows
defverified toto(p) do
IO.inspect p
end
And the inspection of the content of p is correct but the return of the function is a quoted form of my variable.
iex(3)> res = Toto.toto(1)
1
[do: 1]
iex(4)> res
[do: 1]
Is it possible to have an unquoted form for the return of my function or should I unquote it manually?
I expect the following output of my function
iex(3)> res = Toto.toto(1)
1
1
iex(4)> res
1
This happens because the do...end construct is a peculiar piece of syntactic sugar. For example, this:
def toto(p) do
IO.inspect p
end
is equivalent to:
def toto(p), do: IO.inspect p
which, because keywords at the end of an argument get passed as a keyword list, is equivalent to:
def(toto(p), [do: IO.inspect(p)])
Since your defverified macro only expects a do block and no other keyword parameters, we can explicitly match out the actual body:
defmacro defverified(sign, [do: body]) do
...
Before doing this, the last form in the toto function would literally be:
[do: IO.inspect param]
which would call IO.inspect as expected but then return a keyword list instead of a plain value.
I have a macro which gets a module name as parameter and I want to call a function on that module to get some data in order to generate the quote block.
Example:
defmacro my_macro(module) do
data = apply(module, :config, [])
# do something with data to generate the quote do end
end
Obviously, this doesn't work because the parameter value is quoted. I could fetch the data inside the quote block and act accordingly but that would put the whole logic inside the module that uses my macro which is quite dirty. I want to inject as little code as possible.
You can extract the module out by pattern matching with its quoted form: {:__aliases__, _, list} where list is a list of atoms which when concatenated with a dot (use Module.concat/1) produces the full module name.
defmodule A do
defmacro my_macro({:__aliases__, _, list}) do
module = Module.concat(list)
module.foo()
end
end
defmodule B do
def foo do
quote do
42
end
end
end
defmodule C do
import A
IO.inspect my_macro B
end
Output:
42
I am generating some functions in the module:
defmodule M do
funs = [do_it: [:arg1, :arg2]]
Enum.each(funs, fn {name, args} ->
args = Enum.map(args, & {&1, [], Elixir})
def unquote(name)(unquote(args)),
do: IO.inspect(unquote(args))
end)
end
The issue is the generated function obviously accepts one single argument, namely a list of size 2:
▶ M.__info__(:functions)
#⇒ [do_it: 1]
The goal is to dynamically declare the function accepting two arguments. In ruby terminology, it would be to unsplat argument list.
Is there a possibility to accomplish this without pattern matching the resulting AST for {:do_it, blah, [[ list of arguments ]]} and flattening the list manually?
You can use Kernel.SpecialForms.unquote_splicing/1 to "splice" in the args list:
defmodule M do
funs = [do_it: [:arg1, :arg2], do_it: [:arg1, :arg2, :arg3]]
Enum.each(funs, fn {name, args} ->
def unquote(name)(unquote_splicing(args)), do: :ok
end)
end
iex(1)> M.__info__(:functions)
[do_it: 2, do_it: 3]
I'm trying to define two macros with the following code but it failed with ** (CompileError) iex:12: undefined function name/0.
The function parameter name cannot be unquoted in the do block of defmacro.
What is the reason of this ?
Is there any way to solve this?
(Elixir version is 1.2.5)
defmodule IEx.MyHelpers do
def default_env do
__ENV__
end
[:functions, :macros] |> Enum.each(fn name ->
defmacro unquote(name)(option \\ :all) do
import MapSet
quote do
case unquote(option) do
x when x in [:a, :all] -> __ENV__ |> Map.take([unquote(name)])
x when x in [:d, :default] -> default_env |> Map.take([unquote(name)])
x when x in [:i, :imported] ->
difference(new(Map.take(__ENV__, [unquote(name)])),
new(Map.take(default_env, [unquote(name)])))
|> MapSet.to_list
end
end
end
end)
end
You basically need to unquote twice, since the dynamic macro generation already is an implicit macro. You should be fine with adding the following line at the top of your defmacro:
name = unquote(name)