Confusion of :: and <:


#1

happy new year!

I have some confusion around the meaning of :: and <:. Please take a look at the following:

function f(x::Real)
    println("f ", x)
end

function g1(x::Vector{Real})
    println("g1 ", x)
end

function g2(x::Vector{<:Real})
    println("g2 ", x)
end

julia> f(3)
f 3
julia> f(3.2)
f 3.2

julia> g1([3,2])
ERROR: MethodError: no method matching g1(::Array{Int64,1})
julia> g1([3.0,2.0])
ERROR: MethodError: no method matching g1(::Array{Float64,1})

julia> g2([3,2])
g2 [3, 2]
julia> g2([3.0,2.0])
g2 [3.0, 2.0]

We use :: in f() to specify x is a subtype of Real (NOT the type of Real).
While in g2(), we need to use <: (rather than :: ) to specify that the parameter (of the parametric type) is a subtype of Real.
Note that calling g1() always fails.

I have confusion about these notations.

Is it better to fix the meaning of :: to be “the type of”, and <: to be “the subtype of” ? If it’s the case, the following should be allowed (but not now):

julia> function f2(x<:Real)
           println("f ", x)
       end
ERROR: syntax: "x <: Real" is not a valid function argument name

#2

I think the confusion is between instances and subtypes.

x::Real in the signature specifies that x is an instance of Real, not a subtype. 1 is an instance of Real (because its type, Int, is a subtype of Real), so f(1) dispatches to this method.

julia> 1 isa Real
true
julia> 1 <: Real
ERROR: TypeError: in <:, expected Type, got Int64
Stacktrace:
 [1] top-level scope at none:0
julia> typeof(1) <: Real
true

f(Int) will not dispatch to f because Int is not an instance of Real (!(Int isa Real)), although it’s a subtype (Int <: Real).
Similarly, [3,2] isa Vector{Int} <: Vector{<:Real} therefore [3,2] isa Vector{<:Real}, but !([3,2] isa Vector{Real}).
A syntax like function f2(x<:Real) that you suggest seems to imply that this function expects a type as its argument rather than a Real value.


#3

a syntax that distinguishes function f1(x::solidtype) and function f2(x<:abstracttype) is desirable since f1() reads “x is an instance of type solidtype”, and f2() reads “x is an instance of a soild subtype of abstracttype”. Do you agree?


#4

No, your f2 reads "x is a subtype of abstracttype".
By “solid subtype” do you mean “concrete subtype”? If so, your proposed reading of x<:abstracttype in the signature means exactly the same thing as x::abstracttype, since the instances of concrete subtypes of abstracttype are precisely the instances of abstracttype (if x isa concretetype <: abstracttype then it’s also an instance of abstracttype. Conversely, if x isa abstracttype, it’s also an instance of a concrete subtype of abstracttype since every object is an instance of some concrete type).

Do you propose that :: should be written as <: if the type is abstract?


#5

exactly


#6

I see your point (in the first post).
But I think the second definition of g2 might be written in an abbreviated form (although I am not sure how the full form would look like, maybe …Vector{T}) where T::Real ?). Maybe the non-abbreciated definition is more consistent.


#7

As @bernhard is explaining, :: describes a relationship between a value and a type: x::T asserts that x is of type T.

On the other hand, <: is an operator which works on relationships between two types: If S <: T is true then S is a subtype of T. If you change the meaning so that x <: T works, that completely changes the meaning of <:.

What do you then propose as a new operator to describe subtype relationships?


#8

Use magic(obj::T) for specifying a method that takes the arguments typeof(obj) <: T.
Now for parametric types such as containers,

magic(obj::AbstractVector{<:Real})

means it will define methods for all arguments of typeof(obj) <: AbstractVector{<:Real} such as 1:3 which is a UnitRange{Int64}, zeros(3) which is a Vector{Float64}, etc.

In summary, use :: when you are defining the type and <: when specifying the field type of a parametric type.

I am not sure why the magic(obj::AbstractVector{Real}) without the <: parsers without a syntax error…


#9

Maybe I misread you, but why should it be a syntax error?

julia> obj = Real[2, 3.2]
2-element Array{Real,1}:
 2  
 3.2

julia> obj isa AbstractVector{Real}
true

A more elaborate example:

julia> magic(obj::AbstractVector{Real}) = println("Hello, ", typeof(obj))
magic (generic function with 1 method)

julia> magic(obj::AbstractVector{<:Real}) = println("Bye, ", typeof(obj))
magic (generic function with 2 methods)

julia> magic(Real[2, 3.2])
Hello, Array{Real,1}

julia> magic([2, 3.2])
Bye, Array{Float64,1}

#10

Aye, I thought that was deprecated in favor of where syntax, but I see it was kept.


#11

It’s the other way around, unless I misunderstand you: AbstractVector{<:Real} is equivalent to the where syntax, while AbstractVector{Real} is not.


#12

I usually use type dispatch as,

magic(obj::AbstractVector{<:Real}) = 0
magic(obj::AbstractVector{<:AbstractFloat}) = 1
magic(obj::AbstractVector{T}) where T<:Float64 = 2

instead I think I will switch to,

magic(obj::AbstractVector{Float64}) = 2

#13

They are not quite equivalent but probably not in a way that you intended:

julia> magic(Vector{Union{}}())
2

Of course that’s not a very useful vector type since it can’t have any elements.


#14

Unless I’m missing something, I think the original question in this thread doesn’t really have anything to do with :: and <:, but rather with Julia’s invariant subtyping.

In other words, @tomtom was defining a different method than he thought. This:

function g1(x::Vector{Real})
    println("g1 ", x)
end

does in fact define a method that accepts arguments of Vector{Real} type or any valid subtype. However, if you substitute a subtype (Int64) of the parameter (Real) for a parameterized type (Vector{Real}), you do not get a subtype of the parameterized type. So:

julia> Vector <: AbstractArray
true

julia> Int64 <: Real
true

julia> Vector{Int64} <: Vector{Real}
false

which is a departure from many other languages (such as Java and C++).

You can prove this is what’s occurring by forcing the vector type:

julia> g1([1, 2])
ERROR: MethodError: no method matching g1(::Array{Int64,1})
Closest candidates are:
  g1(::Array{Real,1}) at REPL[1]:1
Stacktrace:
 [1] top-level scope at none:0

julia> g1(Real[1, 2])
g1 Real[1, 2]

Note, though, that subtypes of the “outside” type of a parameterized type do count as subtypes (so long as the parameter is of the same type). So:

function g1_1(x::AbstractArray{Int64})
    println("g1_1 ", x)
end


julia> g1_1([1, 2])
g1_1 [1, 2]

julia> typeof([1, 2])
Array{Int64,1}

#15

Is it? Last time I used those languages (which was a while ago), their generics/templates were invariant, like Julia, though Java has covariant arrays. In C++ you can mimic covariance by defining appropriate conversions/copy constructors, but it’s not the default.