For variable number of arguments insertion you mind find looking at Macro.unquote_splicing
helpful.
@kip thanks for the hint
Tried following
defmodule MyMacros do
defmacro gen_fun(name, args) do
quote do
def unquote(name)(unquote_splicing(args)) do
:ok
end
end
end
end
defmodule MyModule do
require MyMacros
fname = :abc
fargs = [:a, :b]
MyMacros.gen_fun(fname, fargs)
end
But it is not compiling!
== Compilation error in file lib/mymodule.ex ==
** (ArgumentError) expected a list with quoted expressions in unquote_splicing/1, got: {:fargs, [line: 30], nil}
(elixir) src/elixir_quote.erl:122: :elixir_quote.argument_error/1
(elixir) src/elixir_quote.erl:103: :elixir_quote.list/2
expanding macro: MyMacros.gen_fun/2
lib/mymodule.ex:30: MyModule(module)
** (exit) shutdown: 1
(mix) lib/mix/tasks/compile.all.ex:59: Mix.Tasks.Compile.All.do_compile/4
(mix) lib/mix/tasks/compile.all.ex:24: anonymous fn/1 in Mix.Tasks.Compile.All.run/1
(mix) lib/mix/tasks/compile.all.ex:40: Mix.Tasks.Compile.All.with_logger_app/1
(mix) lib/mix/task.ex:331: Mix.Task.run_task/3
(mix) lib/mix/tasks/compile.ex:96: Mix.Tasks.Compile.run/1
(mix) lib/mix/task.ex:331: Mix.Task.run_task/3
(iex) lib/iex/helpers.ex:104: IEx.Helpers.recompile/1
It seems I need to do something similar to unquote_splicing(unquote(args))
What am I missing?
The key is is to use bind_quoted
like:
defmodule MyMacros do
defmacro gen_fun(name, args) do
quote bind_quoted: [name: name, args: args] do
def unquote(name)(unquote_splicing(args)) do
:ok
end
end
end
end
Which I think works as you expected. Why is that?
def
is itself a macro. So now we need a way to may unquote
unambiguous. Using bind_quoted: [bindings]
effective disallows unquoting in the current context. And therefore unquote
ing happens from the surrounding context. Its a bit mind-warping I know.
In general its a good idea to use bind_quoted
because it makes clear the parameters to your macro and helps keep everything hygienic. And because José wrote the documentation that says its a good idea
As a follow on: I mentioned that def
is also a macro. Which means that you donât actually need to define a macro as you did before. It definitely better encapsulates your intent and simplifies your code. But as a counter example consider the following:
defmodule MyMacros do
def gen_fun(name, args) do
quote do
def unquote(name)(unquote_splicing(args)) do
:ok
end
end
end
end
defmodule MyModule do=
fname = :abc
fargs = [:a, :b]
Code.eval_quoted MyMacros.gen_fun(fname, fargs), [], __ENV__
end
You can see:
- The use of
unquote
inside a function, not a macro - The
quote
returns Elixir AST as always - So
gen_fun/2
returns an AST that expands to a function definition - Now in the consuming module we have a way to get the AST we want, representing the function definition. But since its not a macro we need a way to insert that AST into the module. If it was defined in a macro then elixir would know to just insert the AST. But in this case its a function return that just happens to be AST.
- So we use
Code.eval_quoted
that actually executes the AST with the side effect that a function is defined.
I am not at all advocating this approach. Just using is as a further example of AST generation and insertion.
Wow, thanks a lot @kip
Code.eval_quoted
took it to a whole new level. I need to learn a lot still it seems . I understand these arenât the way to code readable, maintainable and suntainable way. But stillâŠ
- What other down sides may be with this approach?
- Are there any recommended way of AST generation and insertion?
- Are there any use case where such AST generation and insertion can be recommended design approach (e.g. wrapping all exported functions of an erlang module)
I highly recommend Metaprogramming Elixir by @chrismccord if these are topics youâre into (which seems a safe bet at this point).