Hi I am studying Julia’s type system and was confused about parametric types. In the juliadocs there is the following example
struct Point{T}
x::T
y::T
end
and if I would like to restrict T to <:Real I do the following
struct Point{T<:Real}
x::T
y::T
end
but I cannot do this
struct Point{T where T<:Real}
x::T
y::T
end
or
struct Point{T} where T<:Real
x::T
y::T
end
what is wrong with it? Moreover, if for whatever reason, this is what I intend to do
struct Point{T,S} where S<:Real, T<:S
x::T
y::S
end
how should I do it?
A few other related questions (suppose T, S, etc. are declared types):
Union{T,S} isa Union, but not a DataType, why?
Union isa Type and Union<:Type are both true, So is Union an object of Type, or a subtype of Type? Similiarly Union isa DataType is true, but Union<:DataType is false, what is the rationale?
Type isa Type is true, but T isa T is false. what’s the rationale?
What is the general framework of all these rules? Is it consistent?
Maybe weird, but c’est la vie. Alternatively, you can enforce field type relationships in an inner constructor:
struct Point{T,S<:Real}
x::T
y::S
function Point{T,S}(x, y) where {S<:Real, T<:S}
return new{T,S}(x, y)
end
end
Furthers answers, as per my understanding:
There are 3 “kinds” of types (subtypes(Type), excluding Core.TypeofBottom): DataType, Union and UnionAll. The first includes all normal concrete and abstract types. The second includes all unions of a finite number of types. The third includes constructs like Vector{T} where {T<:Any}.
Type is a supertype of all type objects. Union is a supertype for all union type objects, but it is also an object of DataType. Thus, Union is a subtype of Type, and its type is also a subtype of Type. Union isa DataType means typeof(Union) <: DataType (i.e. Union as an object has a type which is in the DataType subtree). Union <: DataType is just false, they are on different branches of type hierarchy.
Same as previous. T isa T is not true in general because, e.g., Int is not an integer number, it is a type.
is there a mistake in the second case, where instead of t[t3/T]<:t', what it means should be t'<:t[t3/T]?
I am not a CS person so I may be grossly off in my understanding …
In Julia 1.0, on the other hand, type parameters and constructors will be thoroughly consistent, following these general principles uniformly:
The syntax used to define a method always matches the syntax used to call it.
The F{T} syntax always refers to the type F with parameter value T .
Type parameters are always introduced by where clauses.
I think these principles make the syntax and semantics of parametric types and methods far more understandable and intuitive. Given these principles, semantics like what we now have seem inevitable. The only real question is syntax – and what syntax to use was one of the biggest bikesheds around this issue. I wasn’t initially thrilled with the postfix where syntax, but it’s grown on me and now it seems quite natural. The only really odd case, as you mention, is f(...) where T = body without any type bound, but I’ve found that even this case fairly quickly loses its unfamiliarity. Other keywords than where were discussed, including forall and ∀ . However, Julia’s “union all” types are not universally quantified types (they’re actually closer to existentially quantified types), so both of these choices would have been actively at odds with existing type theory nomenclature. The where keyword was the most evocative choice proposed that didn’t clash with well-established terminology. Finally, having the where clause on the right just seemed to read much more naturally in the vast majority of usages.
I see what you mean. When I pulled the quote I was more focused on why the where keyword was introduced. For consistency it would make sense to have imposed it for structs too…
My best guess is that the {...} in a type definition struct MyType{...} is roughly equivalent to the part after the where in a function definition, and where constructs are disallowed in this place:
julia> struct Foo{T where T}
x::T
end
ERROR: syntax: invalid variable expression in "where"
julia> foo(x::T) where {T where T} = x
ERROR: syntax: invalid variable expression in "where"
The point is that the {T} in struct Foo{T} serves the same purpose as the {T} after the where in function foo(::T) where {T}: in both cases, the {T} indicates that the compiler should treat T as a type variable. Writing {T where T} in this position does not make sense because T where T is not a valid type variable symbol.
Similarly, struct Foo{T} where T end does not make sense because you already introduced T as a type variable in the Foo{T} part.
This is just me shooting from the hip, I don’t really understand the deeper reasons here, but to me, these two are analogous:
function foo(x::Number) end
struct Foo{X<:Number} end
And the reason you need the where clause for functions is that you need some way to provide the subtyping information that doesn’t fit in the function signature:
function foo(x::X) where {X<:Number}
For structs, this isn’t necessary, or makes no sense, because it fits right in the struct signature.
It used to be that signatures looked like this
function foo{X<:Number}(x::X)
but that caused problems for type constructors (and some other stuff).
Edit: seems these are equivalent as it overwrites the existing method:
julia> function norm(p::Point{T}) where T<:Real
sqrt(p.x^2+p.y^2)
end
norm (generic function with 1 method)
julia> methods(norm)
# 1 method for generic function "norm":
[1] norm(p::Point{T}) where T<:Real in Main at REPL[8]:1
julia> function norm(p::Point{T} where T<:Real)
sqrt(p.x^2+p.y^2)
end
norm (generic function with 1 method)
julia> methods(norm)
# 1 method for generic function "norm":
[1] norm(p::Point{T} where T<:Real) in Main at REPL[10]:1
Hm. I guess it’s reasonable that struct F{T} where T<:Int would be comparable to struct F{Int} which is not the same as struct F{T<:Int}.
I still think that this is essentially free syntax, and it would be very handy for clearing up type signatures. But maybe it’s better to not introduce confusion about this slight nuance.
Thank you all for the historical info. Could you guys take a look at the quoted paper and help with the question I had in post #3? Thanks.
Btw, does A<:B and B<:A imply A==B?
They’re not quite equivalent; the scope of T differs between the two cases:
julia> foo(::T where T) = T
foo (generic function with 1 method)
julia> bar(::T) where T = T
bar (generic function with 1 method)
julia> foo(1)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] foo(#unused#::Int64)
@ Main ./REPL[1]:1
[2] top-level scope
@ REPL[3]:1
julia> bar(1)
Int64
Not that I’m aware of. In the first form, T has very limited scope.
Here are some additional examples that might help clarify how scoping works with type declarations.
julia> foo(x::Union{T,Vector{T}} where T) = eltype(x) # Works
foo (generic function with 1 method)
julia> foo(x::Union{T,Vector{T} where T}) = eltype(x)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] top-level scope
@ REPL[2]:1
julia> foo(x::Union{T where T, Vector{T}}) = eltype(x)
ERROR: UndefVarError: T not defined
Stacktrace:
[1] top-level scope
@ REPL[3]:1
julia> foo(x::Union{T where T, Vector{T} where T}) = eltype(x) # Also works, each T is different
foo (generic function with 1 method)