While doing some repetitive data evaluation I had the idea to write a simple templating macro to reduce the boilerplate a bit. However my solution does not work and I don’t understand why. Moreover it in fact does work, if I @macroexpand1
the expression once and then evaluate. Here is a MWE:
using DataFrames, DataFramesMeta, Statistics
df = DataFrame(:A => [rand(2,3,4), rand(2,3,4)])
## what I want to do: compute mean and variance of a matrix-valued column
## ofc in the real application there are many columns to be transformed this way...
@rtransform(df, :A_mean = mean(:A), :A_var = var(:A)) # works
## macro-solution
macro mytrafo(df, field)
# need to use these "Meta.quot" to get the ":" in front of symbols
avg_clause = :($(Meta.quot(Symbol(field,"_mean"))) = mean($(Meta.quot(field))))
var_clause = :($(Meta.quot(Symbol(field,"_var"))) = var($(Meta.quot(field))))
return quote
@rtransform($df, $avg_clause, $var_clause)
end |> esc
end
@mytrafo(df, A) # ArgumentError: Malformed expression on LHS in DataFramesMeta.jl macro
eval(@macroexpand1 @mytrafo(df, A)) # works?!
@macroexpand @mytrafo(df, A) # ArgumentError: Malformed expression on LHS in DataFramesMeta.jl macro
The result from @macroexpand
looks a bit off but actually works when copy-pasting:
julia> @macroexpand1 @mytrafo(df, A)
quote
@rtransform df :A_mean = mean(:A) :A_var = var(:A)
end
julia> @rtransform df :A_mean = mean(:A) :A_var = var(:A)
# works
Full error message
julia> @macroexpand @mytrafo(df, A)
ERROR: ArgumentError: Malformed expression on LHS in DataFramesMeta.jl macro
Stacktrace:
[1] fun_to_vec(ex::Expr; gensym_names::Bool, outer_flags::NamedTuple{(Symbol("@byrow"), Symbol("@passmissing"), Symbol("@astable")), Tuple{Base.RefValue{Bool}, Base.RefValue{Bool}, Base.RefValue{Bool}}}, no_dest::Bool)
@ DataFramesMeta ~/.julia/packages/DataFramesMeta/MrIOy/src/parsing.jl:378
[2] fun_to_vec
@ ~/.julia/packages/DataFramesMeta/MrIOy/src/parsing.jl:314 [inlined]
[3] (::DataFramesMeta.var"#44#45"{NamedTuple{(Symbol("@byrow"), Symbol("@passmissing"), Symbol("@astable")), Tuple{Base.RefValue{Bool}, Base.RefValue{Bool}, Base.RefValue{Bool}}}})(ex::Expr)
@ DataFramesMeta ./none:0
[4] iterate(::Base.Generator{Vector{Any}, DataFramesMeta.var"#44#45"{NamedTuple{(Symbol("@byrow"), Symbol("@passmissing"), Symbol("@astable")), Tuple{Base.RefValue{Bool}, Base.RefValue{Bool}, Base.RefValue{Bool}}}}})
@ Base ./generator.jl:47
[5] rtransform_helper(::Symbol, ::Expr, ::Vararg{Expr})
@ DataFramesMeta ~/.julia/packages/DataFramesMeta/MrIOy/src/macros.jl:1599
[6] var"@rtransform"(__source__::LineNumberNode, __module__::Module, x::Any, args::Vararg{Any})
@ DataFramesMeta ~/.julia/packages/DataFramesMeta/MrIOy/src/macros.jl:1638
[7] #macroexpand#63
@ ./expr.jl:119 [inlined]
[8] top-level scope
@ REPL[15]:1
So my core questions here are:
- Why does
@macroexpand1
work and@macroexpand
fail? In my mental model, the latter just repeatedly calls the former until converged. I know that macros are expanded inside out, but precedence should never matter here, since at first there is only my macro and then only the macro fromDataFramesMeta.jl
- How do I work around this? (Maybe clearer with the answer to the question above)
I am probably overlooking something simple, but the @macroexpand(1)
thing really throws me off
EDIT: Happens both under my 1.9.3 and a freshly downloaded 1.10.0