Say I have an abstract type that is parameterized on a TypeVar that will have a concrete integer value.
abstract type A{N} end
I have a struct B where I’d like to subtype A but where I modify the TypeVar that is used.
struct B{M} <: A{M+1}
...
end
Adding 1 is arbitrary. I might also want to multiply M or something like that.
Is it possible to replicate this behavior somehow? Doing it directly does not work, since you can’t constrain M to be of a particular type, like you would do in C++.
That’s impossible because subtyping and field types need to be unchanging, but methods aren’t.* Part of it is M not being constrained, so +(M, 1) can’t be dispatched in advance. But even if you could somehow specify M::ConcreteType, there’s no proof that the method for the call +(::ConcreteType, ::Int) won’t change, even if you are sure it shouldn’t change like in the case of +(::Int, ::Int). Imagine that you had B{3} <: A{4}, but Big Brother tells you 3+1 == 5; B{3} <: A{5} is an unfeasible change to make across all method tables and compiled code.
I think the closest you can do is struct B{M, N} <: A{N} end, throw an error in the inner constructor when N == M+1 is violated, and try to make sure it’s made true in other constructors e.g. B{M}() where M = B{M, M+1}(). Note that this doesn’t constrain N == M+1 for the type itself; with enough effort you can change what the constructors do, and the type parameters won’t need to (and can’t) change to accommodate the constructors’ different constraints.
*Example of constraint in changeable inner constructor
julia> abstract type A{N} end
julia> struct B{M, N} <: A{N}
function B{M, N}() where {M, N}
if !(N == M+1) throw("blah") end
new{M,N}()
end
end
julia> B{3,5} # type itself not constrained
B{3, 5}
julia> B{3,5}() # but constructor constrains instantiation
ERROR: "blah"
Stacktrace:
[1] B{3, 5}()
@ Main ./REPL[12]:3
[2] top-level scope
@ REPL[15]:1
julia> struct B{M, N} <: A{N} # constructor can be changed
function B{M, N}() where {M, N}
if !(N == M+2) throw("blah") end
new{M,N}()
end
end
julia> B{3,5}()
B{3, 5}()
julia> B{M}() where M = B{M, M+2}() # this UnionAll constructor helps constraint
julia> B{3}()
B{3, 5}()
As a C++ programmer, it seems a bit…outlandish to suggest that 3+1 could ever be expected to yield a value other than 4, but such is the way of Julia’s type system, I suppose.
You sure? I’ve never managed to make it work on type parameters in the header alone:
julia> @computed struct B{M} <: A{M+1} end
ERROR: MethodError: no method matching +(::TypeVar, ::Int64)
...
Aside: be aware that ComputedFieldTypes implicitly introduces type parameters, much like B{M, N} earlier. Constructors are omitted so it appears as if only the declared parameters M exist, but B{M} won’t be the concrete type, which can be computed with fulltype(B{M}). If you change any of the methods used to compute the fields’ types, not everything can adjust to B{M} referring to a different type. It can be convenient syntactic sugar, but I ended up preferring not to hide the extra parameters and constructors.
+ can be assigned different functions in different modules without affecting each other, though a different name is probably more sane.