Relation between type parameter

Hi,
I was wondering if there was a mean to enforce some kind of relation between parameter types.
Here is an example:
I can define the structure:

struct mystruct{N,M}
              A::Array{Float64,N}
              B::Array{Float64,M}
 end

but not

struct mystruct2{N}
       A::Array{Float64,N}
       B::Array{Float64,N+1}
end

neither myalias{N} = mystruct{N,N+1}

Is there an elegant way to do that? Is a trait adapted to specialize method when M == N+1?
Thanks

Not directly in type parameters because while a struct type’s structure cannot change, N+1 depends on what the dispatched method of + does, which is allowed to change at runtime. Even though changing the behavior of +(::Int, ::Int) specifically would break Julia, it can’t recognize an exception for a particular call signature. This would be hypothetically possible for a function that can’t change, but we can only define the changing generic functions, and none of the built-ins are supported. For now (and probably very far into the future because of practical issues I haven’t mentioned), the language doesn’t do type parameter computation.

However, you can enforce a relation between type parameters at runtime by checking it in all constructor methods (or rather the inner ones that all the others use), throw an (informative) error instead of allowing instantiation to complete. (EDIT: see next commenter’s example.) There are unsafe ways to get around it to make invalid instances, but undefined behavior is not your problem.

If you don’t enforce that relation, you can also indirectly dispatch to a different method given that relation by mapping subsets of the parametric type to Holy traits. I don’t know a good name so I’ll just go for a typical stand-in:

struct Foo{d} end

isfoo(::mystruct{N,M}) where {N,M} = Foo{M==N+1}()

bar(x::mystruct) = bar(isfoo(x), x)
bar(::Foo{true}, x) = "M==N+1 here!"
bar(::Foo{false}, x) = "uh oh"

So all mystruct{N,M} where M==N+1 are mapped to Foo{true}(), and the rest are mapped to Foo{false}(). We can dispatch over those values’ different types to different methods:

julia> bar(mystruct(zeros(3), zeros(2,3)))
"M==N+1 here!"

julia> bar(mystruct(zeros(3,3), zeros(2,3)))
"uh oh"
3 Likes

Although you cannot constrain the parametric types, note that you can constrain construction:

julia> struct MyStruct{M, N}
           A::Array{Float64, M}
           B::Array{Float64, N}
           function MyStruct(A::Array{Float64, M}, B::Array{Float64, N}) where {M, N}
               if M + 1 != N
                   throw(ArgumentError("Wrong M/N"))
               end
               new{M, N}(A, B)
           end
       end

julia> MyStruct(rand(10), rand(10, 20))
MyStruct{1, 2}([0.6004011295966778, 0.12740683358215132, 0.3939894763092303, 0.07413103266456234, 0.5847503911474503, 0.18333009710427606, 0.3778337994193758, 0.2640461333323526, 0.4879893271040746, 0.8580042570348061], [0.8406780616356467 0.49054628365561614 … 0.5381138878408204 0.056638061595590705; 0.9641422719834767 0.3323178717886266 … 0.16255734342665917 0.9266963224391359; … ; 0.4946863219330464 0.49174041417955083 … 0.6696892296071885 0.9457106674902207; 0.7516719986066737 0.3836054641885388 … 0.5334842120015687 0.8908046734756528])

julia> MyStruct(rand(10), rand(10, 20, 30))
ERROR: ArgumentError: Wrong M/N
Stacktrace:
 [1] MyStruct(A::Vector{Float64}, B::Array{Float64, 3})
   @ Main ./REPL[1]:6
 [2] top-level scope
   @ REPL[3]:1
2 Likes

Thanks,
I was already using Holy traits but I didn’t realized that one can perform operations on parameters of a parametrized method as in Foo{M==N+1}() .

I found a bit annoying to add the bar(x::mystruct) = bar(isfoo(x), x) kind function for every methods and I was looking for some kind of alias. But I guess I have my answer. I’ll mark this answer as a solution noting that @barucden message is a valid solution either.

FWIW, I would do what barucden posted. I don’t use Holy traits much and when I do, it’s not to specialize behavior for a specific relationship among type parameters. I either always enforce a relation or I don’t, but that’s just my experience, and I have very little.

Let me add that Benny’s response and mine complement each other.

If an instance of mystruct{M, N} is valid even in the case of M + 1 \neq N and you are just looking for a way to clearly dispatch on the two cases, M + 1=N and M + 1\neq N, then Benny showed how to do it.

If, on the other hand, it is only allowed to have mystruct{M, N} such that M + 1 = N, then my answer showed how to disallow construction of invalid instances.

1 Like

Indeed, narrowing the span of possible parameters using inner constructor is quite important and should probably highlighted somewhere in the doc, especially when the structure tends to have a large number of parameters to stay as concrete as possible.

1 Like

Narrowing parameters specifically isn’t described in the documentation for inner constructors, but enforcing invariants in general is. The doc’s current example enforces the inputs x, y to obey x <= y for instantiation, so it’s a small hop to doing that for the method’s static parameters, as barucden demonstrated.