Dict subtype definition

I want to define the data type of a dictionary using subtype definition.

I’m trying to do seomthing like
Dict{String, <:Union{String, Vector{String}}}

but if I try
Dict{String, <:Union{String, Vector{String}}}()

I got

MethodError: no method matching (Dict{String, <:Union{String, Vector{String}}})()

The same thing happen if I try
Dict{String,<:Any}()

The problem is that if I run

Dict{String,String} <: Dict{String,<:Any}
it returns true

but if I run
Dict{String,String} <: Dict{String,Any}
it return false

And the same things happens for Dict{String,Union{String, Vector{String}}}

How can I use subtype definition in a dictionary?

You just want

Dict{String, Union{String, Vector{String}}}()

to create a container that can take Strings and Vector{String}s as its values.

The important difference between Foo{Union{Bar, Baz}} and Foo{<:Union{Bar,Baz}} is that the latter is the union over all Foo{T} where T is a subtype of Union{Bar, Baz} (so Foo{T} where T <: Union{Bar, Baz}. That construct is called a UnionAll and is very useful to write parametric code, but cannot be instantiated.

8 Likes

In the first half, Dict{String,<:Any} is equivalent syntax to Dict{String,T} where T<:Any; that is an iterated union a.k.a. UnionAll. Iterated unions are abstract types, so they can’t be instantiated; you can check with isconcretetype:

julia> isconcretetype(Dict{String,<:Any})
false

julia>  isconcretetype(Dict{String,Any})
true

Now for the second half, this is a common mistake, so it’s documented halfway into this section. Note how Dict{String,Any} is a concrete type, and a concrete type cannot be a supertype of another concrete type. The reason it’s a common mistake is that people see the Any and think “isn’t that abstract?” Correct, a concrete parametric type can have abstract types as parameters! It’s commonly a way for Julia to make containers that contain multiple concrete types: Vector{Int} can only hold Int instances, while Vector{Integer} holds references to instances of any subtypes of Integer.

Extras:

  1. It’s worth mentioning that a very important container is an exception to the “concrete parametric type with abstract type parameters” rule: Tuple (and NTuple and Vararg by extension, but not NamedTuple). So Tuple{Integer} is actually abstract and is a supertype of Tuple{Int}. It represents the same set of types as an iterated union Tuple{T} where T<:Integer, but note that it isn’t one. The reason for this special treatment is that Tuple is a Core type intended to package concrete types together in one spot, so there wasn’t much point in allowing abstract type parameters that require references to distant spots. There’s been discussions of possibly removing this special treatment in v2.

  2. there’s a very niche case where a concrete type can be a supertype of an abstract type: Type{Int} <: DataType. It doesn’t have to be Int, it can be any type that is an instance of DataType.

4 Likes

Thanks for the answer. I just have one doubt about the section you linked.

Following the example we have

struct Point{T}
    x::T
    y::T
end

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

So if I call norm(Point{Float64}(1.5,2.5)) everything works as expected. But if I declare the function as following

f

function norm(p::Point{<:Int})
    sqrt(p.x^2 + p.y^2)
end

I expect that norm(Point{Float64}(1.5,2.5)) would not work since Float64 is not a subtype of Int.
Instead it works. Why?

Also in the docs is specified that

Point{Float64} <: Point{Real}

return false event if Float64 <: Real.

Lots of questions

struct Point{T}
    x::T
    y::T
end

function norm(p::Point{<:Int})
    sqrt(p.x^2 + p.y^2)
end

norm(Point{Float64}(1.5,2.5))

results in

ERROR: LoadError: MethodError: no method matching norm(::Point{Float64})

for me. If you try

struct Point{T}
    x::T
    y::T
end

function norm(p::Point{<:Float64})
    sqrt(p.x^2 + p.y^2)
end
function norm(p::Point{<:Int})
    sqrt(p.x^2 + p.y^2)
end

methods(norm)

you’ll see something like

# 2 methods for generic function "norm":
[1] norm(p::Point{<:Float64}) in Main at ...
[2] norm(p::Point{<:Int64}) in Main at ...

And lastly

is documented here.

1 Like

It’s probably because you already defined Point for Real. Defining it for Int just adds a new method on top of the old. You can add new methods on top of each other as many times as you like, that is how you use multiple dispatch.

Restart Julia to clear the old method definitions.

BTW, instead of Point{<:Int}, just write Point{Int}. Int and Float64 have no subtypes, so <:Int is just the same as Int.

2 Likes