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

Here is an old issue: zero() and one() for incomplete and abstract types · Issue #4808 · JuliaLang/julia · GitHub

1 Like

I don’t understand. What words do you use to describe the type Vector{SUBTYPE} where SUBTYPE <: SUPERTYPE ? As far as I can tell, you are describing it.

If SUBTYPE is an abstract type (which is a possibility if e.g SUPERTYPE is Real and SUBTYPE is Integer), then none of the elements give typeof(element) == SUBTYPE because a value can only have a concrete type, not an abstract type.

It suggests that to me too, and as far as I can tell it is that. That single one type can be an abstract type.

I think I see what you mean by “arbitrary”. Did you want that method to be called with T bound to C, like it is for f(x::Vector{T}) where T<:C?

Note the following three relationships:

The relationship between the T in v::Vector{T} and an element of a v, e.g. v[1], is:
v[1] isa T

The relationship between T in foo(x::T) and x is:
x isa T
(note that T must be defined. e.g. const T = Real)

The relationship between T in foo(x::T) where {T} and x is:
x == T

I don’t know if that last relationship is very clearly encoded in the manual generally, but there is an example with same_type: Methods · The Julia Language

If I understood your expected behavior, you can get it like this:

function g(x::T1, y::T2)
    T = typejoin(T1, T2)
    # ...
end

Now x isa T and y isa T.

If you want to constrain T, you’ll (as of now) need to do something like

g(x::T1, y::T2) where {T1, T2} = _g(x, y, typejoin(T1, T2))

function _g(x, y, lub::Type{T}) where T<:C
    return 1
end
julia> g(A(), B())
1

julia> g(1, 2.0)
ERROR: MethodError: no method matching _g(::Int64, ::Float64, ::Type{Real})

Ok, I missed the concrete word there.

Exactly, that is what I feel that is not what it means.

The type of bound is different.

f(x::T,y::T) where T<:Real = 1 

only works if x and y are of the same concrete type. While

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

works even the elements of x have different concrete types. It is what it is, but it could mean something else. And, at this point of my understanding, would be more natural and consistent with the meaning of the f(x::T,y::T) where T if T could only be one concrete type in the container. That would leave the notations

f(x::Vector{<:Real}) = 1

or

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

to the case where one wants to allow vectors of mixed concrete types (UnionAll).

The notation for functions is “complete”, as:

f(x::T,y::T) where T<:Real = 1

works only if x and y are of the same concrete type, and if we want something more general, one can write

julia> f(x::T1,y::T2) where {T1<:Real,T2<:Real} = 1
f (generic function with 1 method)

which is very explicit on what it means.

For a vector if I want my function to work only for vectors that have elements of the same concrete type (Float64[],Float32[],Int[]..., but not Real[] or Numer[]), there is no concise way to write that, and we have to be very verbose, such as

julia> f(x::T) where T <:Union{Vector{Int},Vector{Float32},Vector{Float64}} = 1
f (generic function with 1 method)

The fact that that is considered an issue I think illustrates my point. If that is changed to an error, then it will break codes which rely on zero(T) or one(T) for a function that was defined with the Vector{T} where T parameterization if called with a container of abstract types, which means that the function should be expecting a container “completely concrete” as SK calls them there.

1 Like

I think you bring up a good point.

As an aside, I point out that this is less verbose:

julia> f(x::Vector{T}) where T <:Union{Int,Float32,Float64} = 1
f (generic function with 1 method)

julia> f(Vector{Int}())
1

julia> f(Vector{Real}())
ERROR: MethodError: no method matching f(::Array{Real,1})

Though note that it is a different method signature, and it is “less specific” than the one you proposed. The subtyping (and on top of that, method ordering) in Julia can be unexpected for sure:

julia> T1 = Vector{<:Union{Int,Float32,Float64}}
Array{var"#s12",1} where var"#s12"<:Union{Float32, Float64, Int64}

julia> T2 = Union{Vector{Int},Vector{Float32},Vector{Float64}}
Union{Array{Float32,1}, Array{Float64,1}, Array{Int64,1}}

julia> T1 == T2
false

julia> T2 <: T1
true

Back to your point,

I see, so it would feel more natural if Vector{T} behaved more like the method definition. Not vice-versa.

In other words, the current behavior is:

julia> (1, 1) isa Tuple{T, T} where T
true

julia> (1, 2.0) isa Tuple{T, T} where T
false

julia> [1, 1] isa Vector{T} where T
true

julia> [1, 2.0] isa Vector{T} where T
true

and it would feel more natural if the last value were false. Alternatively, you’d like a type such as Vector{T} where isconcretetype(T)

Looks like this has been discussed a bit here: Explicit concrete type constraint in UnionAll · Issue #30363 · JuliaLang/julia · GitHub

1 Like

Indeed, you are able to express it much more clearly. I just point that is not that “I would like”, I don’t need that as a feature for any reason. As I mentioned, I cannot find any example where a function works with containers of every concrete subtype of an abstract type but would not with a container of mixed types of the same abstract type. Thus, adding that would be one case of overspecialization of the function. (the only tricky thing is that working on mixed containers comes with a performance cost).

In respect to the original poster, I will give an explicit answer here with his example, I am not sure if that was clear at the end:

The reason for

julia> [(x,1),(y,1)] isa Array{Tuple{Stuff,Number},1}
false

is that

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

Note that the array is of Tuple{Stuff,Int64}, which means that this array can only contain tuples composed by those two concrete types. That same array cannot contain a tuple where the second element is a Float64, for example:

julia> v = [(x,1),(y,1)];

julia> push!(v,(x,2.0))
3-element Array{Tuple{Stuff,Int64},1}:
 (Stuff("x"), 1)
 (Stuff("y"), 1)
 (Stuff("x"), 2)

Note that the 2.0 was converted to an Int.

This means that this array contains a constraint for the types of numbers that it can handle.

An array of type Array{Tuple{Stuff,Number},1} can contain different types of numbers:

julia> v = Tuple{Stuff,Number}[(x,1),(x,1)]
2-element Array{Tuple{Stuff,Number},1}:
 (Stuff("x"), 1)
 (Stuff("x"), 1)

julia> push!(v,(x,2.0))
3-element Array{Tuple{Stuff,Number},1}:
 (Stuff("x"), 1)
 (Stuff("x"), 1)
 (Stuff("x"), 2.0)

Therefore, while this last array can contain a greater variety of number types, it does not contain the constraint that the first array carries.

This is why neither one is a subtype of the other. Both have something that the other does not have. In other words, an Array{Tuple{Stuff,Int64},1} is not simply an Array{Tuple{Stuff,Number},1} that happened to only have Int64 numbers inside.

2 Likes