Julia types

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

  1. Union{T,S} isa Union, but not a DataType, why?
  2. 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?
  3. Type isa Type is true, but T isa T is false. what’s the rationale?
  4. What is the general framework of all these rules? Is it consistent?
2 Likes

You can write

struct Point{S<:Real, T<:S}
    x::T
    y::S
end

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:

  1. 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}.
  2. 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.
  3. Same as previous. T isa T is not true in general because, e.g., Int is not an integer number, it is a type.
  4. The best source I am aware of is this paper.
8 Likes
struct Point{T} where T<:Real
x::T
y::T
end

Do you know if this syntax is invalid for some functional, or historic/implementation-details reason?

1 Like

Thanks for pointing me to this paper. I am far from being a CS person, but attempted to understand this part of the paper:


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 …

The whole history can be reconstructed from various discussions linked here. I haven’t read everything, but this explanation is a good one-stop.

Let me quote an excerpt from Stefan’s explanation:

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.

2 Likes

But that quote is inaccurate. You cannot use where syntax to define parametric types.

I am also curious as to why.

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"
1 Like

That’s not very convincing to me. There is nothing where-y about it.

a = :(struct Foo{T} where T end)
a.args[2].head == :where
b = :(struct Foo{T<:Any} end)
b.args[2].head == :curly

So they are definitely syntactically separated.

1 Like

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.

4 Likes

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

1 Like

what bou


function foo(x::SomeType{T}) where T

The documentation also mentions the form

function norm(p::Point{T} where T<:Real)

Does that lead to a different function than

function norm(p::Point{T}) where T<:Real

or is it just a different form of writing it?

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.

1 Like

I think that struct F{T} where T<:Int would be comparable to

function f(t) where t::Int

which also doesn’t work.

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

Is there any way with the first form to reuse the T parameter? E.g. this doesn’t work:

julia> foo(x::T where T, y::T) = x+y
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

Nope, that’s the point. It’s a little like asking for this to work:

julia> let
           let
               x = 1
           end
           x + 1
       end
ERROR: UndefVarError: x not defined
Stacktrace:
 [1] top-level scope
   @ REPL[3]:5

The answer is to pull x out of the inner scope into the outer one.

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)
1 Like