[ANN] ConcreteStructs.jl - Cut the boilerplate when concretely parameterizing structs

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.

62 Likes

You are a good human.

34 Likes

Oh, this is very nice! Would it be possible to support type restrictions? E.g.

@concrete struct MyStruct
    a
    b<:Number
    c<:AbstractVector
end

as an equivalent for

struct MyStruct{A,B<:Number,C<:AbstractVector}
    a::A
    b::B
    c::C
end

Or may even (I know, now I’m getting greedy)

@concrete struct MyStruct
    a
    b<:Number
    c<:AbstractVector{typeof(b)}
end

as an equivalent for

struct MyStruct{A,B<:Number,C<:AbstractVector{B}}
    a::A
    b::B
    c::C
end
7 Likes

If not, I have a very similar macro that supports this implemented in one of my packages. Here is the entire implementation and docstring. It doesn’t support inner constructors though which ConcreteStructs does. So it may be nice to combine the 2 implementations.

Edit: would also be nice to make it play nicely with @with_kw of Parameters.jl.

2 Likes

Mind reader! This was what I was scratching my head about only yesterday.

2 Likes

I wonder why isn’t it the default behaviour, what is the reason dispatch only looks in the outer type parameters?

4 Likes

Think of mutable types with fields that start as nothing and then get assigned a value. If the type of the field is Nothing, you can’t assign it properly.

1 Like

Looks like several of us have the same feature request :joy:

3 Likes

@oschulz It seems that there is enough desire for this feature that it’s worth considering. I laid out my objections here, but I feel like this would still be doable in some form. On the “for it” side, it is worth considering that both you and @willtebbutt proposed the same syntax independently.

1 Like

I’d definitely like this to play more nicely with other struct definition macros, especially Base.@kwdef and @with_kw.

2 Likes

Thanks for considering this!

I’ve often found myself wishing for this that as a language feature, i.e. automatic anonymous type parameters for fields defined as fieldname<:SomeType. I assume something like that must have been discussed already, though?

Could you elaborate for newbies like me why this is important (maybe in the readme)? I would guess it has something to do with otherwise having to encapsulate otherwise untyped objects which causes slow runtime checks. But that does not seem right, as I imagine such runtime checks would be rare enough to not matter?

1 Like

Yeah definitely. This page of the docs describes why you would want to parametrize struct fields. It’s definitely worth linking to this in my README, though. Thanks for the suggestion.

2 Likes

The other day I started working on the same idea, with the same name too! Thanks a lot for sharing; what a time saver.

1 Like