I have a question regarding a code snippet from part 6 of a really great Macros guide by @sasajuric.
This question specifically ranges from use of Macro.escape
’s unquote: true
option. to the disparate prioritisation of expansion of def calls compared to other macros.
deftraceable
(as seen in the snippet) is a custom macro, so the arguments sent to it are quoted, creating nested quoting when it is called a def do block
. I presume this nested quoting prevents the unquote on its arguments from taking immediate effect, which means what is passed to deftraceable
should be the AST
of the unquotes and their args.
Expansion of the deftraceable
macro takes place before execution of its housing function, but this isn’t an issue since the macro itself does not do any evaluations on its inputs; however, after conclusion of expansion, bind_quoted will evaluate the arguments passed, and inject an AST describing and binding the results of evaluation to the stipulated variables.
Since the evaluation is going to yield the AST of the unquote and its arguments, the use of Macro.escape
is employed to ensure correct AST form.
I’ve ascertained from the guide and the docs that unquote: true
is necessary here because Macro.escape var, unquote: true
is supposed to recognise where an unquote call AST is present in the evaluation and do a further eval by which it yields the result of the unquote call as the final AST to be injected.
The one small problem I’m having here is that I don’t know how to understand injection and transfer as distinct phenomena in the context described here.
Injecting the code vs transferring data
Another problem we’re facing is that the contents we’re passing from the macro to the caller’s context is by default injected, rather then transferred. So, whenever you do
unquote(some_ast)
, you’re injecting one AST fragment into another one you’re building with aquote
expression.Occasionally, we want to transfer the data, instead of injecting it. Let’s see an example. Say we have some triplet, we want to transfer to the caller’s context…
def
is a macro, so its arguments, including the do block
, are quoted.
I was also a bit curious as to why, ostensibly, no such precaution (Macro.escape var, unquote: true
) need be taken with use of unquote in passing arguments to def. I read something in the guide about def being the exception in the prioritsation of macro expansion, which I assume would mean that the unquote
AST might get to be processed differently when it comes to the involvement of def in dynamic code generation? I would appreciate even a brief clarity on this, as I’ve tried looking at the def code directly and didn’t really get far.
deftraceable Snippet
defmodule Tracer do
defmacro deftraceable(head, body) do
# This is the most important change that allows us to correctly pass
# input AST to the caller's context. I'll explain how this works a
# bit later.
quote bind_quoted: [
head: Macro.escape(head, unquote: true),
body: Macro.escape(body, unquote: true)
] do
# Caller's context: we'll be generating the code from here
# Since the code generation is deferred to the caller context,
# we can now make our assumptions about the input AST.
# This code is mostly identical to the previous version
#
# Notice that these variables are now created in the caller's context.
{fun_name, args_ast} = Tracer.name_and_args(head)
{arg_names, decorated_args} = Tracer.decorate_args(args_ast)
# Completely identical to the previous version.
head = Macro.postwalk(head,
fn
({fun_ast, context, old_args}) when (
fun_ast == fun_name and old_args == args_ast
) ->
{fun_ast, context, decorated_args}
(other) -> other
end)
# This code is completely identical to the previous version
# Note: however, notice that the code is executed in the same context
# as previous three expressions.
#
# Hence, the unquote(head) here references the head variable that is
# computed in this context, instead of macro context. The same holds for
# other unquotes that are occuring in the function body.
#
# This is the point of deferred code generation. Our macro generates
# this code, which then in turn generates the final code.
def unquote(head) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
function_name = unquote(fun_name)
passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
result = unquote(body[:do])
loc = "#{file}(line #{line})"
call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
IO.puts "#{loc} #{call}"
result
end
end
end
# Identical to the previous version, but functions are exported since they
# must be called from the caller's context.
def name_and_args({:when, _, [short_head | _]}) do
name_and_args(short_head)
end
def name_and_args(short_head) do
Macro.decompose_call(short_head)
end
def decorate_args([]), do: {[],[]}
def decorate_args(args_ast) do
for {arg_ast, index} <- Enum.with_index(args_ast) do
arg_name = Macro.var(:"arg#{index}", __MODULE__)
full_arg = quote do
unquote(arg_ast) = unquote(arg_name)
end
{arg_name, full_arg}
end
|> Enum.unzip
end
end
dynamic code generation Snippet
defmodule Test do
import Tracer
fsm = [
running: {:pause, :paused},
running: {:stop, :stopped},
paused: {:resume, :running}
]
for {state, {action, next_state}} <- fsm do
deftraceable unquote(action)(unquote(state)), do: unquote(next_state)
end
deftraceable initial, do: :running
end