I would like to do define a macro that allows me sum together vector-like structs, whose fields might be heterogeneous in terms of their types. I tried something like the following inside of a module:
"""
A heterogeneous vector. Actually the heterogeneity comes from the fields having different units from Unitful.jl, but this is a simplified example. I will have a few different kinds of types like these, and wish to reduce allocations that StaticArrays.FieldVector introduces due to the type heterogeneity.
"""
struct StateVector
a :: Int
b :: Float64
end
"""
This defines addition for FieldVectors.
"""
macro defineAdd(type)
return quote
T = $(esc(type))
fields = fieldnames(T)
sumExprs = [:(x.$f + y.$f) for f in fields]
function Base.:+(x::T, y::T)
T($(sumExprs...))
end # function
end # quote
end # macro
However, this does not work and I get the following error, where M is the module I am defining this in:
julia> M.@defineAdd M.StateVector
ERROR: LoadError: UndefVarError: `sumExprs` not defined in `M`
Suggestion: check for spelling errors or missing imports.
Stacktrace:
[1] var"@defineAdd"(__source__::LineNumberNode, __module__::Module, type::Any)
@ M ~/M.jl/src/M.jl:26
in expression starting at REPL[2]:1
I know I could just define an abstract supertype for these different vectors and call propertynames or fieldnames on those, but this causes allocations. I then need to generate things like
function Base.:+(a::StateVector,b::StateVector)
StateVector(
a.a + b.a,
b.a + b.b,
)
end # function
Sure. But I think my main problem now comes from the macro being defined in a module. All of the examples are always performed in the REPL, so I’m not sure how to proceed.
Your immediate problem is that you try to interpolate something into a quote that was not defined before the quote but inside the quote.
Your main problem is that macros don’t have access to values when they are expanded, so they cannot query the fields of the type you give as argument.
This can be solved with a generated function, which has the critical distinction from normal macros in that it has access to type information.
Another approach that should be possible is to generate the add function together with the struct definition, with a macro
@struct_with_add struct StateVector
a :: Int
b :: Float64
end
Any examples of passing struct definitions to macros? I think the generated function approach can be a bit problematic, because essentially want to do this, if I took that approach.
"""
Summation for StateVectors.
"""
@generated function Base.:+(left::T,right::T) where T <: FieldVector
propNames = fieldnames(T)
return :(
T(
[:(left.$p + right.$p) for p in propNames ]
)
)
end # function
results in an error
ERROR: The function body AST defined by this @generated function is not pure. This likely means it contains a closure, a comprehension or a generator.
Stacktrace:
[1] top-level scope
@ REPL[25]:1
"""
Summation for StateVectors.
"""
@generated function Base.:+(left::T,right::T) where T <: FieldVector
@show fieldNames = fieldnames(T)
@show fieldAdditionExprs = [ :(left.$n + right.$n) for n in fieldNames ]
return :(
T($fieldAdditionExprs...)
)
end
ERROR: MethodError: no method matching StateVector(::Expr, ::Expr)
I don’t know the scope of your project but be prepared that you might gain runtime speed at the cost of compilation time, which may or may not be a favorable tradeoff.