Struggling with signatures

Hi,

I’m struggling with type signatures. The following code only works if all the types I pass are of the exact same type.

abstract type S end

struct S1 <: S
       value::Int64
end

struct S2 <: S
       value::Float64
end

struct Container
       elements::Dict{S, Vector{S}}
       Container() = new(Dict{S, Vector{S}}())
end

function Container(source::Dict{T, Vector{T}} where {T <: S})
       return Container()
end

So,

Container(Dict(S1(1) => [S1(2), S1(3)]))

works, but the following results in the accompanying error.

Container(Dict(S1(1) => [S1(2), S2(3)]))

ERROR: MethodError: no method matching Container(::Dict{S1,Array{Any,1}})
Closest candidates are:
  Container() at none:3
  Container(::Dict{T,Array{T,1}} where T<:S) at none:1
Stacktrace:
 [1] top-level scope at none:1

Digging a bit deeper I found that I can create the following:

d = Dict{S, Vector{S}}()

to which I can add a mixture of elements of types S1 and S2.

I want to avoid having to write the explicit types all the times though so I would like to solve it in the function signature. Is this possible?

When creating a collection with several elements of different types, the type of the collection is set to Any. I wonder whether it would be an idea to use a common type ancestor if possible. That would probably mean an alteration to the language though I guess.

Thanks in advance,
Stef

This is called the Diagonal Rule.

In general, if you use a type parameter (e.g., T) in your signature, Julia expect all values to match the same type. Moreover, type parameters inside parametrict types (in your case, both uses, as T is a parameter for Dict and a parameter for Vector) are considered invariant. This means Julia will not accept a supertype (or subtype) over the specified type, only the exactly specified type.

For example, this will work:

julia> Container(Dict(S1(1) => [S1(2), S2(3)], S2(4) => [S1(5), S2(6)]))
Container(Dict{S,Array{S,1}}())

But only because, the key of the Dict is inferred to be S (because it has both S1 and S2 and S is the narrowest type that matches both) and every Vector value is also inferred to have elements of type S. So both types match exactly. If you let a single vector have only elements of one type the code breaks.

julia> Container(Dict(S1(1) => [S1(2), S2(3)], S2(4) => [S1(5)]))
ERROR: MethodError: no method matching Container(::Dict{S,Array{T,1} where T})

The reason is simple. You already created a Dict of the wrong type at this point because you relied on inference, and because one Vector was inferred to Vector{S} and another to Vector{S1} Julia probably tried to use something like Union{Vector{S},Vector{S1}} (or Any). You can fix it, but it would need to make a copy, what I think it is not what you want. See below.

I see that you have locked the container to the type S (instead of leaving it with a parametric type). I am not sure if this is intended, but I will consider it is intended. The problem boils to the fact you already have structures of the wrong types, Dict{S1, ...} is not a Dict{S, ...} these are distinct types (this is related to the invariance I mentioned above).

function Container(source::Dict{T, Vector}) where {T <: Any}
       # and here create a vector of the right type (Vector{Tuple{S, Vector{S}}})
       # to pass to the Dict constructor and have a Dict of the right type
       # Note the inner value vectors need to be converted from
       # whichever type of vector (like `Vector{S1}`) to `Vector{S}`.

The question is not that the Julia language definition says Any will be used but that the compiler is free to infer anything that is valid. In my first sample code above, with Julia 1.5.3, it inferred S. The language creators did not specify this on purpose, to have more leeway in the way inference is implemented in the compiler. The inference just need to be valid, not tight.

Final observation: you are aware that your last constructor always create empty containers and throw away the arguments, right?

3 Likes

Thank you. This clarifies things for me.
Yes, I am aware that the last constructor throws away the arguments. I created the minimal amount of code to show what I was trying to get my head around. The actual code is more elaborate but contains a lot of stuff which is besides the point.

1 Like