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 Int64s, 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 :neutral_face: (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 :grin:.

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 :slight_smile: ).

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 :grin:, but not in the strict sense of Julia :sweat_smile:.

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 :rofl:

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 ?
:rofl:

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