The Problem
Defining concretely parameterized structs involves quite a bit of boilerplate code. Consider the following definition from DifferentialEquations.jl:
struct ODEFunction{iip,F,TMM,Ta,Tt,TJ,JVP,VJP,JP,SP,TW,TWt,TPJ,S,TCV} <: AbstractODEFunction{iip}
f::F
mass_matrix::TMM
analytic::Ta
tgrad::Tt
jac::TJ
jvp::JVP
vjp::VJP
jac_prototype::JP
sparsity::SP
Wfact::TW
Wfact_t::TWt
paramjac::TPJ
syms::S
colorvec::TCV
end
The only type parameter that you explicitly care about here is the first one, iip
. The rest are just there to make sure ODEFunction
is concretely typed. It’s a little annoying that you have to come up with type parameter names for types you don’t really care about, right? And what’s more, parameterizing structs too early on makes it a bit of a pain to change your code later (did I remember to put a new type parameter in when I added a field to my struct definition?). Wouldn’t it be cool if there was a macro to just do this for you automatically?
A Solution
ConcreteStructs.jl exports the macro @concrete
that will add type parameters to your struct for any field where type parameters aren’t given. So in that ODEFunction
example, we could get the same result just by saying:
@concrete struct ODEFunction{iip} <: AbstractODEFunction{iip}
f
mass_matrix
analytic
tgrad
jac
jvp
vjp
jac_prototype
sparsity
Wfact
Wfact_t
paramjac
syms
colorvec
end
This also works with more complicated parameter dependencies like:
@concrete struct MyType{T,N,A<:AbstractArray{T,N}} <: AbstractArray{T,N}
a::A
b::T
c
function MyType(a::AbstractArray{T,N}, b::B, c) where {T,N,B}
Tnew = promote_type(T, B)
a, b = Tnew.(a), Tnew(b)
return new{Tnew,N,typeof(a),typeof(c)}(a, b, c)
end
end
Additionally the @concrete
macro supports a keyword terse
that will cause the struct type to print compactly in the :compact => true
mode of an IOContext
. The current implementation looks like this (but there is an issue open to decide on alternative implementations because I’m not totally convinced this is the best way to go):
@concrete terse mutable struct AnotherType{C}
a::Symbol
b
c::C
end
julia> x = AnotherType(:eyyy, 1f0, (1,2.0))
AnotherType(:eyyy, 1.0f0, (1, 2.0))
julia> typeof(x)
AnotherType{Tuple{Int64,Float64},Float32}
Note that the given type parameters always come first in the type signature (the type of c
here comes before the type of b
because c
’s type was explicitly given.
Anyway. Let me know what you think or if you have any suggestions for improvement here.