Arithmetic for parametric type values in structs e.g., N+1

How can I do something like:

struct Foo{N}
    arr1::Array{Float64, N}
    arr2::Array{Float64, N+1}
end

i.e., I want arr2 to be always of 1 higher rank than arr1. How to do this?

Maybe there’s a fancier way but this seems to work fine.

julia> struct Foo{A,B}
           a::A
           b::B
           Foo{A,B}(a,b) where {A,B} = ndims(a)+1 == ndims(b) ? new{A,B}(a,b) : error("wrong dims")
       end

julia> (Foo(a::A,b::B) where {A,B}) = Foo{A,B}(a,b)
Foo

julia> Foo([1], [1;;2])
Foo{Vector{Int64}, Matrix{Int64}}([1], [1 2])

julia> Foo([1], [1])
ERROR: wrong dims
1 Like

It is impossible for the composite type to do parameter constraints other than <: in the implicit where clause, you need to enforce it in an inner constructor call like jar1 demonstrated. The reason is that generic functions like + can depend on input types and can be changed interactively, so a constraint based on a generic method could also change interactively, but a composite type CANNOT change because of existing instances. That’s also why you need the “redundant” parameter B==A+1; after a constraint change, the type system must distinguish previous Foo{1, 2} instances from newer Foo{1, 42} instances.

The inner constructor should be enough, it is unsafe (in other words, user beware) to bypass inner constructors with reinterpret or @eval @inline outnew(T, args...) = $(Expr(:splatnew, :T, :args)) #:new for an unsplatted number of separate arguments. I am uncertain if it is hypothetically possible to extend constraints to intrinsic and built-in functions, which I don’t think can change.

1 Like

I would do the following.

julia> struct Foo{N,M}
           arr1::Array{Float64, N}
           arr2::Array{Float64, M}
           Foo{N}(a,b) where N= new{N, N+1}(a,b)
       end

julia> Foo{1}(rand(2), rand(2,3))
Foo{1, 2}([0.9165330134722103, 0.8133055136507046], [0.15120619990261142 0.8543356012671888 0.5239972992036701; 0.12074486400173157 0.6817097391005016 0.3327645622931258])
3 Likes

That’s even neater and closer matches the OP, and I’d suggest rounding it out with an outer constructor like jar1 did for convenience:

julia> Foo(a, b) = Foo{ndims(a)}(a, b)
Foo

julia> Foo(rand(2), rand(2,3))
Foo{1, 2}([0.7928217615317612, 0.27627055084273566], [0.8947120194686449 0.7069448391898014 0.628333742859724; 0.3193547716322238 0.7802926781126777 0.9817486584424423])

and incorporating a more descriptive error message in the inner Foo{N} than a conversion error in new. Note this approach consciously sacrifices a Foo{N, M} constructor, so the output of typeof can’t instantiate.

2 Likes

I like ConstructionBase.constructorof for getting constructors instead of typeof.