Passing computed list to an Elixir macro - macros

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__
)

Related

Can I expand a macro inside a quote of another macro in Elixir?

Given the below code, the line use Composite, user_opts: user_opts ends up as [{:user_opts, [line: 3, counter: {MockUserNode1, 2}], Automaton.Node}] inside the using(opts) as opts. Is there any way to inject that code inside the outer macro?
defmacro __using__(user_opts) do
a =
if Enum.member?(Composite.types(), user_opts[:node_type]) do
IO.inspect(user_opts)
quote bind_quoted: [user_opts: user_opts] do
use DynamicSupervisor
use Composite, user_opts: user_opts
end
else
quote do: use(Action)
end
end
Answering the question stated: it’s perfectly possible to call macros from inside other macros, the just inject the AST recursively in the end.
defmodule DeeplyUsed do
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
opts
end
end
end
defmodule Used do
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
use DeeplyUsed, opts: opts
end
end
end
defmodule Using do
use Used, line: 3, counter: {MockUserNode1, 2}
end
That said, your issue is induced.
Sidenote: [{:user_opts, _, Automaton.Node}] looks indeed very suspicious there, that’s not how keyword lists are being quoted. Start with explicit unquoting and logging what comes to user_opts there.
defmacro __using__(user_opts) do
IO.inspect(user_opts, label: "Outside")
quote do
IO.inspect(unquote(user_opts), label: "Inside")
use Composite, user_opts: unquote(user_opts)
end
end

How to dynamically generate a Phoenix controller function with a map argument?

So I'm using Phoenix 1.3, and I created a macro to generate a function and inject it into a controller.
Based on the number I pass in, I want it to generate a map with that many parameters that are named "id1", "id2", etc. all the way up to "id#{number}". This map will be part of the argument list along with the usual Phoenix "conn".
So I want to generate a method like this to be pattern matched against and "some stuff" can be executed:
def index(conn, %{"id1" => id1, "id2" => id2}) do
# some stuff
end
when I call the macro create_some_function_by_number("index", 2).
My macro looks something like:
defmacro create_some_function_by_number(name, num) do
params =
for n <- 1..num, do: %{"id#{n}" => Macro.var(:"id#{n}", nil)}
|> Map.new
quote do
def unquote(:"#{name}")(unquote(Macro.escape(params)) do
# some code here for the index action
end
end
end
1) How do I inject the "conn" into the function head so it can be pattern matched against?
2) Is this the correct way to create the map to be pattern matched against?
While you can definitely use macros in this way, you probably should not. Here is a working solution with comments:
defmodule MyMacro do
defmacro create_some_function_by_number(name, num, do: block) do
params =
for n <- 1..num do
{"id#{n}", Macro.var(:"id#{n}", nil)}
end
# We can't call Macro.escape because it is for escaping values.
# In this case, we have a mixture of values "id#{n}" and
# expressions "Macro.var(...)", so we build the map AST by hand.
pattern =
{:%{}, [], params}
conn =
Macro.var(:conn, nil)
quote do
def unquote(:"#{name}")(unquote(conn), unquote(pattern)) do
unquote(block)
end
end
end
end
defmodule MyExample do
import MyMacro
create_some_function_by_number :index, 2 do
{conn, id1 + id2}
end
end
IO.inspect MyExample.index(:conn, %{"id1" => 1, "id2" => 2})
As you can see, macros can make the code harder to understand. If you can solve it at runtime, it should definitely be preferred.

Elixir Macros: Get unquoted parameter value outside of quote

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

Elixir Macro issue with unquoting

Can you please have a look at my Macro?
I am getting undefined function number/0 error, and I can't figure it out why.
defmodule DbUtil do
defmacro __using__(opts) do
quote do
import unquote(__MODULE__)
#before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(%{module: definition} = _env) do
quote do
import Ecto.Query
def last do
from x in unquote(definition), order_by: [desc: x.id], limit: 1
end
# This dumps error
def limits(number) do
from a in unquote(definition), limit: ^unquote(number)
end
end
end
end
You don't need to unquote number. unquote is used when you want to inject a variable present outside the quote block. Since number is defined inside the quote, you don't need to unquote. The following should work for you:
def limits(number) do
from a in unquote(definition), limit: ^number
end

Double unquoting for dynamically generated macros

Given the following:
for fn_name <- [:foo, :bar, :baz] do
defmacro unquote(fn_name)(do: inner) do
fn_name = unquote(fn_name) # <--- Why?
quote do
IO.puts "#{unquote(fn_name)} called"
unquote(inner)
end
end
end
What's the reason for fn_name = unquote(fn_name)? If I omit this line, it's a compile error. What's the reason for this "double" unquoting?
Let's simplify the example a little bit:
for fn_name <- [:foo, :bar, :baz] do
defmacro unquote(fn_name)(do: inner) do
fn_name = unquote(fn_name) # <--- Why?
quote do
{unquote(fn_name), unquote(inner)}
end
end
end
In the example above, because quote is returning a tuple with two unquoted elements, it is equivalent to:
for fn_name <- [:foo, :bar, :baz] do
defmacro unquote(fn_name)(do: inner) do
fn_name = unquote(fn_name) # <--- Why?
{fn_name, inner}
end
end
Now it is easier to understand what happens if you don't unquote(fn_name) before: the variable fn_name simply wouldn't exist inside the macro definition. Remember that all defs (def, defp, defmacro, etc) start a new variable scope, so if you want to use fn_name inside, you need to define it somehow.
The other property we are seeing in this code is that Elixir will stop unquoting when it sees a quote. So in the quote above, unquote won't be unquoted when the macro is defined but rather when the macro is executed, which also explains why the variable is required to be defined inside the macro.
It's because of Hygiene.
Elixir has the concept of macro hygiene. Hygiene means that variables, imports, and aliases that you define in a macro do not leak into the caller’s own definitions.
for fn_name <- [:foo, :bar, :baz] do
defmacro unquote(fn_name)(do: inner) do
fn_name = unquote(fn_name) # <-- This is macro's context
quote do
IO.puts "#{unquote(fn_name)} called" # <-- This is caller's context
unquote(inner)
end
end
end
You should read Hygiene Protects the Caller’s Context from Chris McCord's Metaprogramming Elixir book