Question about function argument


#1

I have a question about the function argument types. Here is my example.

julia> r = [(0, 0), (0, 0)]
2-element Array{Tuple{Int64,Int64},1}:
 (0, 0)
 (0, 0)

julia> f1(r::Vector{NTuple{2}}) = length(r)
f1 (generic function with 1 method)

julia> f2(r::Vector{NTuple{2, <:Real}}) = length(r)
f2 (generic function with 1 method)

julia> f3(r::Vector{NTuple{2, T}}) where {T<:Real} = length(r)
f3 (generic function with 1 method)

julia> f1(r)
ERROR: MethodError: no method matching f1(::Array{Tuple{Int64,Int64},1})
Closest candidates are:
  f1(::Array{Tuple{T,T} where T,1}) at REPL[38]:1

julia> f2(r)
ERROR: MethodError: no method matching f2(::Array{Tuple{Int64,Int64},1})
Closest candidates are:
  f2(::Array{Tuple{#s16,#s16} where #s16<:Real,1}) at REPL[39]:1

julia> f3(r)
2

My question is why f1 and f2 didn’t work and how are these three functions different? Thanks!


#2

You are hitting invariance. Have a read through this https://docs.julialang.org/en/stable/manual/types/#Parametric-Composite-Types-1 and report back if you’re still confused (likely).


#3

Also try this:

julia> f2(r::Vector{<:NTuple{2, Real}}) = length(r)
f2 (generic function with 1 method)

julia> f2(r)
2

#4

I am sure someone else can do a better job, but here is my attempt.

First you can find out what works and what doesn’t by doing:

julia> typeof(r)<: Array{Tuple{s1,s1},1} where s1<:Real
true

julia> typeof(r)<: Array{Tuple{s1,s1} where s1<:Real,1}
false

julia> typeof(r)<: Array{Tuple{T,T} where T,1}
false

This is explained as type invariance in the docs:
https://docs.julialang.org/en/stable/manual/types/

For f2 the input is mainly a vector of pointers that can hold Real, and f1 is a a vector of pointers that hold anything. Because the parameters vary inside the {}. For f3 the type has to be the same (outside the {}), and can be inferred statically to be Int64 in this case so it works.

Examples:

julia> bla=Array{Tuple{T,T} where T}(2)
2-element Array{Tuple{T,T} where T,1}:
 #undef
 #undef

julia> bla[1]=r[1];bla[2]=("cat","dog")
("cat", "dog")
julia> f1(bla)
2

Hope this helps.


#5

Thanks a lot for all your answers. I read the manual before and read it again last night. I think I learnt a lot more.

I understood before that the position of where keyword affects the scope of the type variables. In Vector{NTuple{2, T} where T}, the T is local and cannot be seen outside of Vector{} and T can change from Tuple to Tuple in the Vector, whereas in Vector{NTuple{2, T}} where T, the T can be accessed in the method and all Tuple in the Vector must have the same element type T. What I didn’t know is that the first one declares a concrete type and the second declares an abstract type. That explains why f1 and f2 didn’t work because Array{Tuple{Int64,Int64},1} is a concrete type and cannot be a subtype of another concrete type.

This all makes sense now except I still don’t understand why Vector{NTuple{2, T} where T} declares a concrete type and Vector{NTuple{2, T}} where T declares an abstract type. Is this just by definition? or is there some reasoning behind this behavior? If so, what is the reasoning? Thanks!


#6
julia> f2(r::Vector{<:NTuple{2, Real}}) = length(r) 
f2 (generic function with 1 method)

Thanks for this wonderful example. After carefully reading the manual, I realize this is covariant type and also unlike other types, Tuple types are covariant as well , so Tuple{2, Real} is an abstract type. Vector{<:NTuple{2, Real}} is the same as Vector{T} where T<:NTuple{2, Real} and Tuple{Int64,Int64} is a subtype of NTuple{2, Real}. That is why your example works.


#7

One way to understand it is to think about what an abstract type is – in Julia, it’s a type that cannot be instantiated, and potentially has many concrete subtypes.

For example, Real (representing a real number) is an abstract type; you can’t instantiate one, and it has many concrete subtypes – among them Float32, Float64, Rational:

julia> 0.5
0.5

julia> typeof(0.5)
Float64

julia> 1 // 2
1//2

julia> typeof(1 // 2)
Rational{Int64}

julia> Float64 <: Real
true

julia> Rational{Int64} <: Real
true

Now, here’s the trick – the type Real is the same exact type as the type T where T <: Real: they both specify precisely the same set of concrete types. You can see this simplification at the REPL:

julia> T where T <: Real
Real

With a parametric type like Vector the same kind of logic applies – Vector{T where T <: Real} is the same as Vector{Real} and is the concrete type of a list that contains real numbers of any kind:

julia> v = Vector{Real}()
0-element Array{Real,1}

julia> push!(v, 1.0, 1//2)
2-element Array{Real,1}:
  1.0
 1//2

In contrast, the type Vector{T} where T <: Real, or Vector{<:Real} for short, cannot be concrete because it has many subtypes, one for each concrete subtype of Real:

julia> Vector{Float64} <: (Vector{T} where T <: Real)
true

julia> Vector{Rational} <: (Vector{T} where T <: Real)
true

# and many more...

#8

Thanks for the explanation! It is very helpful. To complete your example.

julia> Vector{<:Real}()
ERROR: MethodError: no method matching Array{#s17,1} where #s17<:Real()

Vector{<:Real} cannot be instantiated.