Argument which is an array of the same subtypes don't match array of super-type

The short version is that a function typed as

function x1(Array{SuperType,1})

will throw an error if an argument which is an array of only subtypes is passed but not if the array is composed of elements with a mix of subtypes, i.e. if we have subtypes A1 and A2, x1([A1, A2]) works, but x1([A1,A2]) does not work.

I’m not quite sure I understand why.

Here’s a concrete example to demonstrate.

abstract type Foo end

struct Bar <: Foo
    a :: Int

end

struct Baz <: Foo
    a :: Float64
end

function x1(z::Array{Foo,1})
    println(z)
end

w1 = Bar(1)
w2 = Baz(1.0)

w = [ w1, w2 ]

x1(w)

w  = Foo[w1, w1]

x1(w)

w = [w1, w1]

x1(w)
Foo[Bar(1), Baz(1.0)]
Foo[Bar(1), Bar(1)]
briand@quarternote:~/src/julia/fdtd$ julia test5.jl
Foo[Bar(1), Baz(1.0)]
Foo[Bar(1), Bar(1)]
ERROR: LoadError: MethodError: no method matching x1(::Array{Bar,1})
Closest candidates are:
  x1(::Array{Foo,1}) at /home//src/julia/fdtd/test5.jl:12
Stacktrace:
 [1] top-level scope at /home//src/julia/fdtd/test5.jl:29
 [2] include(::Function, ::Module, ::String) at ./Base.jl:380
 [3] include(::Module, ::String) at ./Base.jl:368
 [4] exec_options(::Base.JLOptions) at ./client.jl:296
 [5] _start() at ./client.jl:506
in expression starting at /home//src/julia/fdtd/test5.jl:29

https://docs.julialang.org/en/v1/manual/types/#Parametric-Composite-Types has the very relevent warning

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

Adapting for this case, what you want is function x1(Array{<:SuperType,1}) which can be written more simply as function x1(Vector{<:SuperType})

3 Likes

I think I may have chosen my example poorly. I think that in my example that Bar and Baz could be defined as parametric types, like Bar{T} so I would have Bar{Int} and Bar{Float64}, but what if Bar and Baz are quite distinct, e.g.

struct Bar <: Foo
  a :: SomeOtherType
end
struct Baz <: Foo
  b :: YetAnotherType
  c :: AndYetAgain
end

It doesn’t matter if the subtypes are completely distinct, you just need to make sure that they both support whatever interface f uses.

abstract type Foo end

struct Bar <: Foo
    a :: String
end

struct Baz <: Foo
    b :: Int64
    c :: Float64
end

doFoo(a::Array{<:Foo}) = foreach(println, a)

aBar = [Bar("cow"), Bar("dog")]
aBaz = [Baz(5,sqrt(5)), Baz(6,sqrt(6))]
aFoo = [Bar("goldfish"), Baz(7,0.3)]

doFoo(aBar)
doFoo(aBaz)
doFoo(aFoo)

You just wouldn’t want something like,

doMoreFoo(a::Array{<:Foo,1}) = println(a[1].a)

which would fail for an array of all Baz and would have questionable efficacy for an array of Foo.

1 Like

I understand the your example but I’m still not clear on why a declaration of

f(a::Array{Foo,1}

fails when I pass it Array{Bar,1} or Array{Baz,1}.

Here’s a much simpler example…

julia> f(x::Array{Number,1}) = x[1] + x[2]
f (generic function with 2 methods)

julia> f([1,2])
ERROR: MethodError: no method matching f(::Array{Int64,1})
Closest candidates are:
  f(::Array{Number,1}) at REPL[4]:1
  f(::Number, ::Number) at REPL[1]:1
Stacktrace:
 [1] top-level scope at REPL[5]:1

huh ???
An array of Int64 is an array of Number, isn’t it ? It seems very unintuitive that this causes an error.

Notice how the following works,

julia> f(x::Number, y::Number) = 2x-y
f (generic function with 2 methods)

julia> f(2,3)
1

julia> f(2.0, 3.0)
1.0

Why the array example fails, but this example works seems logically inconsistent.

And just for the sake of completeness,

julia> f(x::Array{<:Number}) = x[1] + x[2]
f (generic function with 3 methods)

julia> f([1,2])
3

oh. drat. I think i get it…

Array{<:Number} means an array, each element of which, must be a subtype of Number.
So, Array{Number,1} means something different obviously, but I’m not getting how exactly it’s different…

1 Like

Let’s look at an example. Both Array{Number, 1} and Array{Int, 1} are concrete types, and no concrete type can inherit from another in Julia.

An instance of an Array{Number, 1} is an array that you can insert any number into, and can contain any kind of numbers.

This is a property not shared by an Array{Int, 1} which can only have Ints inserted into it, and which only can contain Ints.

They are also represented differently with Array{Int, 1} directly containing Int while Array{Number, 1} contains pointers to Number's

1 Like

The important difference in a glance:

julia> isconcretetype(Array{Number, 1})
true
julia> isconcretetype(Array{<:Number, 1})
false

The first is a concrete Array that “contains” an abstract parametric type by actually containing pointers to concrete subtypes of that abstract parametric type. Your method simply worked on the concrete type it was made for. Run println(typeof(w)) for each array you have, you’ll see.
The second is an abstract type that’s nowhere as specific. It can be an Array of contiguous Int64 values, it can be an Array of contiguous Float64 values, or it can be an Array of pointers. In fact:

julia> Array{Number, 1} <: (Array{<:Number, 1})
true

This confused me at first too, but it’s just a matter of knowing what the syntax is really saying. I actually prefer the equivalent but more verbose syntax Array{T, 1} where T <: Number just to help myself tell the difference from Array{Number, 1}.

3 Likes

yay! I finally get it :smiley:

Thanks very much to everyone for helping me to understand !

1 Like