Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Purpose of "where" keyword in constructor of a parametric type

For the Julia manual parametric composite type example

struct Point{T}
    x::T
    y::T
end

it is possible to write an outer constructor such as

Point(x::T, y::T) where {T} = Point{T}(x, y)

Why is the where {T} part needed, i.e. why isn't

Point(x::T, y::T) = Point{T}(x, y)

possible? Julia complains that T is not defined, but could it not figure out that T is a type from the :: syntax? I am a newcomer to Julia so I might be missing something pretty basic.

like image 486
miha priimek Avatar asked Jun 22 '20 13:06

miha priimek


2 Answers

T is a variable that has to be defined. If you do not define it in where Julia starts looking for it in an outer scope. Here is an example:

julia> struct Point{T}
           x::T
           y::T
       end

julia> Point(x::T, y::T) = Point{T}(x, y)
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] top-level scope at REPL[2]:1

julia> T = Integer
Integer

julia> Point(x::T, y::T) = Point{T}(x, y)
Point

julia> Point(1,1)
Point{Integer}(1, 1)

Note that in this example you do not get what you expected to get, as probably you have hoped for Point{Int64}(1,1).

Now the most tricky part comes:

julia> Point(1.5,1.5)
Point{Float64}(1.5, 1.5)

What is going on you might ask. Well:

julia> methods(Point)
# 2 methods for type constructor:
[1] (::Type{Point})(x::Integer, y::Integer) in Main at REPL[4]:1
[2] (::Type{Point})(x::T, y::T) where T in Main at REPL[1]:2

The point is that The (::Type{Point})(x::T, y::T) where T already got defined by default when the struct as defined so with the second definition you have just added a new method for T = Integer.

In short - always use where to avoid getting surprising results. For example if you would write:

julia> T = String
String

julia> Point(1,1)
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type String

you get a problem. As the signature of Point is fixed when it is defined but in the body T is taken from a global scope, and you have changed it from Integer to String, so you get an error.


Expanding on what @Oscar Smith noted the location of where is also sensitive to scope, and tells Julia on what level the "wildcard" is introduced. For example:

ulia> T1 = Vector{Vector{T} where T<:Real}
Array{Array{T,1} where T<:Real,1}

julia> T2 = Vector{Vector{T}} where T<:Real
Array{Array{T,1},1} where T<:Real

julia> isconcretetype(T1)
true

julia> isconcretetype(T2)
false

julia> T1 isa UnionAll
false

julia> T2 isa UnionAll
true

julia> x = T1()
0-element Array{Array{T,1} where T<:Real,1}

julia> push!(x, [1,2])
1-element Array{Array{T,1} where T<:Real,1}:
 [1, 2]

julia> push!(x, [1.0, 2.0])
2-element Array{Array{T,1} where T<:Real,1}:
 [1, 2]
 [1.0, 2.0]

and you can see that T1 is a concrete type that can have an instance, and we created one calling it x. This concrete type can hold vectors whose element type is <:Real, but their element types do not have to be the same.

On the other hand T2 is a UnionAll, i.e. some of its "widlcards" are free (not known yet). However, the restriction is that in all concrete types that match T2 all vectors must have the same element type, so:

julia> Vector{Vector{Int}} <: T2
true

julia> Vector{Vector{Real}} <: T2
true

but

julia> T1 <: T2
false

In other words in T2 the wildcard must have one concrete value that can be matched by a concrete type.

like image 97
Bogumił Kamiński Avatar answered Oct 18 '22 18:10

Bogumił Kamiński


I'm going to take a different tack from Bogumił's excellent answer (which is 100% correct, but might be missing the crux of the confusion).

There's nothing special about the name T. It's just a common convention for a short name for an arbitrary type. Kinda like how we often use the name A for matrices or i in for loops. The fact that you happened to use the same name in the struct Point{T} and in the outer constructor is irrelevant (but handy for the readers of your code). You could have just as well done:

struct Point{SpecializedType}
    x::SpecializedType
    y::SpecializedType
end
Point(x::Wildcard, y::Wildcard) where {Wildcard <: Any} = Point{Wildcard}(x, y)

That behaves exactly the same as what you wrote. Both of the above syntaxes (the struct and the method) introduce a new name that will behave like a "wildcard" that specializes and matches appropriately. When you don't have a where clause, you're no longer introducing a wildcard. Instead, you're just referencing types that have already been defined. For example:

Point(x::Int, y::Int) = Point{Int}(x, y)

That is, this will reference the Int that was already defined.

I suppose you could say, but if T wasn't defined, why can't Julia just figure out that it should be used as a wildcard. That may be true, but it introduces a little bit of non-locality to the syntax, where the behavior is drastically different depending upon what happens to be defined (or even exported from a used package).

like image 6
mbauman Avatar answered Oct 18 '22 18:10

mbauman