# Why? isa([(x,1),(y,1)], Array{Tuple{Stuff,Number},1}) = false

Hello,

I do not understand the output of this script:

``````struct Stuff
a
end

x = Stuff("x")
y = Stuff("y")

test1 = isa(1, Number)
test2 = isa((x,1), Tuple{Stuff,Number})
test3 = isa([(x,1),(y,1)], Array{Tuple{Stuff,Number},1})

println(test1)
println(test2)
println(test3)
``````

The output is:

``````true
true
false
``````

I would have expected:

``````true
true
true
``````

Since this is what this other snippet produces:

``````...
test1 = isa(1, Int64)
test2 = isa((x,1), Tuple{Stuff,Int64})
test3 = isa([(x,1),(y,1)], Array{Tuple{Stuff,Int64},1})

println(test1)
println(test2)
println(test3)
``````

Could you help me to understand the reason of this behaviour?

Thanks,

Michel

Related: Why are tuples covariant?

Consider

``````test4 = isa([(x,1),(y,1)], Array{<:Tuple{Stuff, Number},1})
``````
1 Like

`Number` is an `abstract type`, whereas

``````julia> isa([(x,1),(y,1)], Array{Tuple{Stuff,Int64},1})
true
``````

Complementing the previous answers, there is no guarantee about how Julia will infer the type of an Array/Vector literal with elements of many distinct types inside. Using a single type like you did will most probably get an Array/Vector of the specific type, but keep in mind that if you mix types the result may be either the specific `Union` of the mixed types, or a supertype of all the mixed types (what in many cases, end up being `Any`).

2 Likes

Further explanation for a newbie: the key reason is that `Array` is not covariant (or more generally, " Julia’s type parameters are invariant"; see the link above).

A more concise example to show the invariance:

``````julia> 1 isa Real
true

julia> [1]
1-element Vector{Int64}:
1

julia> [1] isa Vector{Real}
false

julia> Int64 <: Real
true

julia> [1] isa Vector{<:Real} # <: means any subtype of Real
true
``````
4 Likes

Covariance and etc. mean so many things outside computer science that it took me a while to get what people where saying here.

I prefer to explain, probably not as comprehensively, but at least simply, by noting that:

First, we have to differentiate two things:

a) An array that can only contain numbers of type `Float64`
b) An array that can contain real numbers of different types (mixed `Float64` and `Int64`, for example).

Vectors of type (b) are not a subtype of vectors of type (a), of course, because vectors of type (a) cannot contain an `Int64`, for example. This is clear and translates to:

``````Vector{Real} <: Vector{Float64} == false
``````

Less clear is that an array of type (a) is also not a subtype of an array of type (b). This is because an array of type (a) has a constraint that vectors of type (b) do not. Thus, a vector of type (a) is not a subtype of vectors of type (a), and this translates to the more unnatural

``````Vector{Float64} <: Vector{Real} == false
``````

Second, the usual confusion is that `Vector{Real}` is intuitively thought as all types of vectors that contain real numbers. Well, this is the wrong way of reading that. As pointed above, `Vector{Real}` is the type of a concrete vector that is able to contain any type of real number. Thus, this does not include the vectors that cannot contain `Int64`s, for instance.

We need a notation for the set of vectors that may contain real numbers, restricted or not by type. The notation might sound arbitrary, but we need one, and it is `Vector{<:Real}`. Since this is the notation that encompasses different types of vectors, it is an abstract type*, contrary to the other two above, which are concrete types.

No actual vector is, therefore, of type `Vector{<:Real}`. To be very redundant:

``````julia> typed(Real[1,2.0,π,Float32(7)]) == Vector{<:Real}
false
``````

But all vectors that contain only real numbers, are subtypes of `Vector{<:Real}`:

``````julia> typeof(Real[1,2.0,π,Float32(7)]) <: Vector{<:Real}
true

julia> typeof(Int[1,2,3]) <: Vector{<:Real}
true
``````

When one uses `Vector{<:Real}` we are referring a set of types. The final confusion that may arise, is, for example, that:

``````julia> typeof(Int64[1,2,3]) == Vector{<:Int64}
false
``````

This is `false` because `Vector{<:Int64}` is the set of types of vectors that contain only `Int64` numbers. It is not a concrete type of vector, even if the set contains only one type which is `Vector{Int64}`.

Of course:

``````julia> typeof(Int64[1,2,3]) <: Vector{<:Int64}
true
``````

*Strictly speaking, in the Julia language, something like `Vector{<:Real}` is of the `UnionAll` type, which is something in between between a completely abstract type which only serve as nodes in the type tree, and a concrete type which can actually be instantiated. `UnionAll` types do have information on how they should be instantiated, by that information is not complete.

(note: the final form of this post has contributions from others, given below).

5 Likes

Thanks
Had forgotten to say I a beginner in Julia.
Now reminds me a bit of Scala …

1 Like

That’s actually `true`, because `typeof(Real[1,2.0,π,Float32(7)]) === Vector{Real} <: Vector{<:Real}`.

``````julia> Real[1,2.0,π,Float32(7)] isa Vector{<:Real}
true
``````

(`x isa T` is equivalent to `typeof(x) <: T`, not `typeof(x) == T`).
I think you meant to point out that `typeof(Real[1,2.0,π,Float32(7)]) !== Vector{<:Real}`.
Same with the `Int64[1,2,3] isa Vector{<:Int64}` example.

2 Likes

Actually I had written correctly and changed it… I did not notice that `isa` is not `typeof() ==`. Didn’t like that (fixing the post). Thanks.

Seems quite natural to me that `x isa T` is `typeof(x) <: T`. After all, 1 is a number, so it makes sense that `1 isa Number` .

2 Likes

Yes, you are right. Probably there is no better choice for that notation. That is a little bit more confusing with containers (yet I like it again ).

1 Like

Indeed correct. The principle of covariance was a little confusing to me at the beginning as well especially with backgrounds in other languages like C#.

An additional comment is that `isabstractype` and `isconcretetype` can be used to check whether a type is abstract or concrete.

``````julia> isabstracttype(Vector{Real})
false

julia> isconcretetype(Vector{Real})
true
``````

We see that `Vector{Real}` cannot even be inherited, i.e., no (strict) subtypes.

However, the following statement may not be exact:

because

``````julia> isconcretetype(Vector{<:Real})
false

julia> isabstracttype(Vector{<:Real})
false
``````

`Vector{<:Real}` is an abstract type in the general sense , but not in the strict sense of Julia .

``````isabstracttype(T)
Determine whether type T was declared as an abstract type
(i.e. using the abstract keyword).
``````
1 Like

I for one find it quite not-arbitrary.

``````julia> Vector{<:Real}
Vector{var"#s46"} where var"#s46"<:Real (alias for Array{var"#s46", 1} where var"#s46"<:Real)
``````

Using a named type parameter makes it easier to read:

``````julia> Vector{T} where T <: Real
Vector{T} where T<:Real (alias for Array{T, 1} where T<:Real)
``````

meaning the `Union` of all types `Vector{T}`, where `T` ranges over all types that are subtypes of `Real`.

Indeed that is what a `UnionAll` type is:

``````julia> typeof(Vector{<:Real})
UnionAll
``````

Good point. But is there a third category of types which does not fit into `concrete` or `abstract`? Shoudn’t every type which is not concrete be abstract by definition? Is that just an implementation detail?

With that I do not agree. Actually the parametric syntax is even more confusing. `Vector{T} where T<:Real` for me clearly suggests that `T` is one of the types which are subtypes of `Real`. This is particularly confusing because the behaviour is somewhat contradictory if `T` is the parameterization of the types in a vector or the types of the arguments of a function:

``````julia> abstract type C end

julia> struct A <: C end

julia> struct B <: C end

julia> f(x::Vector{T}) where T<:C = 1
f (generic function with 1 method)

julia> f([A(),B()])
1

julia> g(x::T,y::T) where T<:C = 1
g (generic function with 1 method)

julia> g(A(),B())
ERROR: MethodError: no method matching g(::A, ::B)
Closest candidates are:
g(::T, ::T) where T<:C at REPL[6]:1
Stacktrace:
[1] top-level scope at REPL[7]:1

``````

Because of that we end up not having, as far as I know, a concise syntax to indicate vectors of one of the single types which are subtypes of a supertype. I understand that that is not very useful, as any function that is able to operate on a vectors of every subtype of `Real` (for example) is able to operate on a vector of mixed types of `Real` (likely with a performance penalty for run time dispatch). Yet that leads to some arbitrary behaviours for those functions, such as:

``````julia> h(x::Vector{T}) where T<:Real = zero(T)
h (generic function with 1 method)

julia> typeof(h(Real[1,2.0]))
Int64

``````

This seems unexpected at the first sight. However, note that

``````julia> p(x::Vector{T}) where T<:Real = T
p (generic function with 1 method)

julia> p(Real[1,2.0])
Real
``````

Thus, the inferred type of `T` is `Real` as we have expected.

The seemingly weird behavior of `h` is due to the following fact:

``````julia> typeof(zero(Real))
Int64
``````

It was the first time I noticed that `zero(Real)` gave an `Int64`. IMHO, `zero(Real)` should raise an error, since it cannot actually determine the concrete type.
Maybe we should @ a Julia language designer for clarification, but I don’t know who

2 Likes

If that raised an error the parameterization should not accept any kind of vectors of mixed types, which goes in to the direction of my point that `Vector{T} where T<:Real` should better be a vector of one of the subtypes of `Real`. Any change on that will be of course be breaking, but I would be more comfortable if the error was raised here already:

given the fact that `T` in the function definition is not a concrete type. Note that if we stick with

``````julia> f(x::Vector{<:Real})
``````

we are not tempted to use `zero(T)` inside the function (we can do of course `zero(eltype(x))` with the same ambiguity, and that could throw an error perhaps (or just return any `zero`, as it is, because it will be promoted whenever necessary).

Edit: If `zero(Real)` returned anything else than `zero(Int)`, a function using that would never return an Int, as one could expect from, for example

``````f(x::Vector{T}) where T<:Real = zero(T) + 1
``````

Yes, I agree. These are mostly corner cases. I have never used any mixed-type vectors. For now, an assertion (or error) may help to forbid non-concrete types.

``````julia> h(x::Vector{T}) where T<:Real = @assert isconcretetype(T)
h (generic function with 1 method)

julia> h([1, 2, 3])

julia> h(Real[1, 2.3])
ERROR: AssertionError: isconcretetype(T)
Stacktrace:
[1] h(::Array{Real,1}) at .\REPL[40]:1
[2] top-level scope at REPL[41]:1
``````
1 Like

Would anybody do a summary …
Or suggest the best 2 readings on the topic ?

Sorry… I think the answers to your question ended here. . What follows is interesting and may be helpful after one gets more or less used to the rules there, but we are diverging from the original question.

I do believe you are searching for the UnionAll types.

1 Like