Parametric constructor where type being constructed is parameter

I have multiple structs (FooKey and BarKey), each of which has its own type parameter (FooType and BarType). Can I write the constructor for these structs in a parametrized manner? Here’s my attempt:

abstract type AllType end

abstract type FooType <: AllType end
abstract type BarType <: AllType end

abstract type AllKey{T <: AllType} end

struct FooKey{T <: FooType} <: AllKey{T}
    x::Int
end

struct BarKey{T <: BarType} <: AllKey{T}
    x::Int
end

M(::Type{T}, x::Int) where {T <: AllType, M <: AllKey{T}} = M{T}(x)

FooKey(FooType, 1)  # should return FooKey{FooType}(1)
BarKey(BarType, 2)  # should return BarKey{BarType}(2)

But if I do that, Julia treats the function name M as a different M than the type parameter.

M is a type, so it should only be inferred in the type domain. However, Julia disallows directly dispatching methods on a type, but only on type parameters. For example:

(::M)(args...) where {M} = (M, args) # Illegal

(::Val{M})(args...) where {M} = (M, args) # Legal

Based on your code, I think this might be the solution close to your needs:

julia> struct KeyHolder{K<:AllKey}
           KeyHolder(::Type{K}) where {K<:AllKey} = new{K}()
       end

julia> function (::KeyHolder{K})(::Type{T}, x::Int) where {T<:AllType, K<:AllKey}
           sym = nameof(K)
           constructor = getfield(Main, sym) # Replace `Main` with whatever Module you defined sub-types of `AllKey` in
           constructor{T}(x)
       end

julia> KeyHolder(FooKey)(FooType, 1)
FooKey{FooType}(1)

julia> KeyHolder(BarKey)(BarType, 2)
BarKey{BarType}(2)
1 Like

It also disallows ::Function and ::Any in the callable position, just too general to implement method tables. Restricting M<:Integer or some other type won’t work for method tables either, the signature demands a const name or an annotation of a type in the callable position.

And that’s also why M gets treated as a const name. There is actually a way to say “a local variable M for inputs that subtype the parametric AllKey”:

julia> (M::Type{S} where S<:AllKey)(::Type{T}, x::Int) where {T <: AllType} = M{T}(x)

julia> FooKey(FooType, 1)
FooKey{FooType}(1)

julia> BarKey(BarType, 1)
BarKey{BarType}(1)

Type is a type, so its annotation is legal in the callable position. Type relations in where clauses strictly apply to type parameters in type annotations, not argument names. Same thing applies to positional arguments, and we actually get an informative error:

julia> bar(M) where M<:Integer = M
ERROR: syntax: function argument and static parameter name not distinct: "M" around REPL[14]:1
Stacktrace:
 [1] top-level scope
   @ REPL[14]:1

julia> bar(X::Type{M} where M<:Integer) = X
bar (generic function with 1 method)

julia> bar(Int), bar(UInt16)
(Int64, UInt16)
1 Like