For illustration, let us say I have the following macros computing rows in a truth table:
macro bool_to_lit(a)
eval(a) ? (x -> x) : (x -> !x)
end
macro make_clause(xs, bools, res)
lits = map((x -> @eval @bool_to_lit $x), bools.args)
clause_elements = map.(lits, xs.args)
and_res = all(push!(clause_elements, res))
return and_res
end
#@make_clause((false, false), (false, false), true) returns true
@bool_to_lit
returns a closure depending on the value of its argument, and @make_clause
uses the result to compute values of its own. However, since @make_clause
uses @eval
, my understanding is that it actually runs @bool_to_lit
(and therefore does not just perform a syntactic transformation).
Would it be better (as in faster and generating cleaner code) to avoid use of nested @eval
in cases such as this, such that the entire result of the whole macro tree is evaluated only once at rutime?
Is it a tradeoff between easier coding (i.e. treating nested macros as functions when @eval
is used) vs. correctness (i.e. only compile-time syntactic transformations when avoiding nested @eval
)?
(Disclaimer: I shortened the logic of your code a bit. Maybe there's an error I made doing that, but the general point is the same.)
In most cases, no, you shouldn't use eval
in a macro. There are two possible alternatives to what you gave. First, if you require the macro only to work on literal booleans (ie., the values true
and false
), then these are stored right in the AST, and you can do normal computations directly at compile time:
julia> macro make_clause_literal(xs, bools, res)
clause_elements = map((lit, arg) -> lit == arg, bools.args, xs.args)
res && all(clause_elements)
end
@make_clause_literal (macro with 1 method)
julia> @macroexpand @make_clause_literal((false, false), (false, false), true)
true
There should be some checking added if the inputs really are literal booleans.
On the other hand, if you want to put in other expressions at well, transform the code to something efficient that does the same thing, and leave evaluation to runtime:
julia> macro make_clause(xs, bools, res)
clause_elements = map((lit, arg) -> :($lit == $arg), bools.args, xs.args)
esc(:($res && $(foldr((e,f) -> :($e && $f), clause_elements))))
end
@make_clause (macro with 1 method)
julia> @macroexpand @make_clause((false, false), (false, false), true)
:(true && (false == false && false == false))
julia> @macroexpand @make_clause((false, false), (false, x), y)
:(y && (false == false && x == false))
Constructing a sequence of &&
should be as good as it gets in terms of avoiding intermediate arrays and short circuiting.
A third option, which I would recommend, is to write a normal runtime function doing the clause evaluation, and rewriting either of the above macros in terms of a call to that. I leave that as an exercise. You could also do a combination of both approaches and evaluate the expression at compile time as far as possible, but I guess the compiler will already do that to some extent.