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!

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

1 Like

Also try this:

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

julia> f2(r)
2
``````
1 Like

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.

1 Like

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!

1 Like
``````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.

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...
``````
3 Likes

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.

1 Like