Dictionaries with abstract types for keys/values as function arguments

I am currently writing a library that for a number of applications needs to take a dictionary of matrices as arguments. In order to make the functions reasonably general, I wanted to make the key/value types in the function arguments abstract. e.g. something like:

function print_dict(dict::AbstractDict{<:Real, AbstractArray{<:Real, 2}})
    print(dict)
end

(This is not a real function I want to write, just for demonstration of the argument type)

Calling this function with a dict of what I assumed to be a valid subtype of the function argument

dict = Dict{Int, Array{Float32, 2}}()

dict[1]  = [10.3 3.4 ; 2.3 4]

print_dict(dict)

however gives the following error:

ERROR: LoadError: MethodError: no method matching print_dict(::Dict{Int64,Array{Float32,2}})
Closest candidates are:
  print_dict(::AbstractDict{var"#s1",AbstractArray{var"#s2",2} where var"#s2"<:Real} where var"#s1"<:Real) at ***/dict_weirdness.jl:1
Stacktrace:
 [1] top-level scope at ***/dict_weirdness.jl:10
 [2] include(::String) at ./client.jl:457
 [3] top-level scope at REPL[1]:1
in expression starting at ***/dict_weirdness.jl:10

I expected this to work as you can do things like AbstractArray{<:Real, 2} as functional argument types no problem.

Can anyone explain why this does not work, and perhaps suggest any workarounds / better practises?

Thanks in advance, Michael

It’s not a complete answer, but:

julia> d=Dict([1 => [1.0 2.0;]])
Dict{Int64,Array{Float64,2}} with 1 entry:
  1 => [1.0 2.0]

julia> print_dict4(d :: AbstractDict{T1, Matrix{T2}}) where {T1 <: Real, T2 <: Real} = println(d);

julia> print_dict4(d)
Dict(1 => [1.0 2.0])

whereas

julia> print_dict5(d :: AbstractDict{T1, AbstractMatrix{T2}}) where {T1 <: Real, T2 <: Real} = println(d);

julia> print_dict5(d)
ERROR: MethodError: no method matching print_dict5(::Dict{Int64,Array{Float64,2}})
julia> function print_dict(dict::AbstractDict{<:Real, <:AbstractArray{<:Real, 2}})

           print(dict)

       end

works. (now I have to catch a bus, maybe someone else can explain more)

2 Likes

That’s the point I was missing:

julia> d=Dict([1 => [1.0 2.0;]])
Dict{Int64,Array{Float64,2}} with 1 entry:
  1 => [1.0 2.0]

julia> isa(d, Dict{<:Real,<:Matrix})
true

julia> isa(d, Dict{<:Real,Matrix})
false

and so the dispatch fails without the additional <:

1 Like

Thank you both, this is enlightening. For some reason, I had not expected the need of <: before the AbstractArray. Typically you don’t need this when the function argument is of AbstractArray type, e.g.

print_array(array::AbstractArray{<:Real, 2}) = println(array)

will work fine without the additional <:

But you do have the additional <: in your last example – it’s the one in front of the Real. This is really the point made in the docs in a warning box:

This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real} .

Ah okay yes I see.

Is the reason you don’t need the <: on the first level of argument type a syntactic nicety then?

i.e. I mean that

print_array(array::AbstractArray{<:Real, 2}) = println(array)

does not need to be

print_array(array::<:AbstractArray{<:Real, 2}) = println(array)

This is just the same fact that Julia types are invariant (https://docs.julialang.org/en/v1/manual/types/#Parametric-Composite-Types) all over again.

If you have a parametric type Foo{T} and types A <: B, then it is not true that Foo{A} <: Foo{B}. It is, however, true that:

Foo{A} <: (Foo{X} where X <: B)

and, equivalently, that:

Foo{A} <: Foo{<:B}

In both cases you are saying "Foo{A} is a subtype of the collection of all types Foo{X} where X is some subtype of B".

In your case, Array{Float64, 2} <: AbstractArray{<:Real, 2}, but that does not mean that Dict{Int, Array{Float64, 2}} <: Dict{Int, AbstractArray{<:Real, 2}}.

Instead, you can say:

Dict{Int, Array{Float64, 2}} <: (Dict{Int, A} where A <: AbstractArray{<:Real, 2})

or equivalently:

Dict{Int, Array{Float64, 2}} <: Dict{Int, <:AbstractArray{<:Real, 2}}

which means, roughly, "Dict{Int, Array{Float64, 2}} is a member of the set of all types Dict{Int, A} in which A is some kind of AbstractArray{R, 2} and R is some kind of Real.

5 Likes