Hi. I am new to Julia and have extremely basic computer science knowledge. I am trying to learn about parametric typing. I’ve done a reasonable amount of internet search and read the documents. Here are two cases which confuse me. Case 1: Both struct definitions work, but the constructor in the second case throws an error. Case 2: The second struct definition does not work. Can someone explain what’s going on? I welcome links to documentation, but would also appreciate an explanation.
# 1
julia> struct S
a::Type{Real}
end
julia> S(Real)
S(Real)
julia> struct S{T<:Type{Real}}
a::T
end
julia> S(Real)
ERROR: TypeError: in S, in T, expected T<:Type{Real}, got Type{DataType}
# 2
julia> struct SS{T<:Int64}
a::T
end
julia> struct SS{T::Int64}
a::T
end
ERROR: syntax: invalid variable expression in "where" around REPL[7]:1
the T1<:T2 syntax means “the type T1 is a subtype of the type T2”
the x::T syntax means “the object x is an instance of the type T”
In the former, you’re comparing two types, which is what you want when putting constraints on the type parameter. In the latter, you’re comparing an object with a type, which explains why it doesn’t work to specify the type of a.
As a side note, Int64 is what’s called a concrete type, so it has no descendants, nor can it have any. This means your definition is equivalent to:
There’s some voodoo around Type, it’s special in some ways. I wouldn’t worry about it as a newcomer. If you have a pragmatic question, ask about it, but poking around Type hasn’t really enlightened me either.
That said, FWIW, you could instantiate your first struct with
julia> S{Type{Real}}(Real)
S{Type{Real}}(Real)
What happens is that the default constructor probably uses typeof to fill in the types, and typeof(Real) is DataType. To me it really feels like it should be Type{Real}, but… (most likely performance-related) voodoo.
You could add a constructor for your type
julia> struct S{T<:Type{Real}}
a::T
S(::Type{T}) where T = new{Type{T}}(T)
end
julia> S(Real)
S{Type{Real}}(Real)
While Type is part of Julia’s type hierarchy like any other abstract parametric type, it is not commonly used outside method signatures except in some special cases.
Don’t use it in struct parameters, there’s no reason to.
@gdalle , thanks for the note on the syntax. My take away is that you have to use the <: syntax to compare types, even when you’re comparing concrete types (which have no subtypes). Maybe that’s why I was trying the second approach in case 1. And the struct was already getting defined accurately (thanks for your suggestion though @DNF ), it’s just that I wasn’t using the right constructor, as @cstjean pointed out. Thanks to you all. This helped me.
Edit: @DNF pointed out that I misunderstood what @cstjean said, so the rest of this comment is no longer relevant.
@cstjean, I am not sure I can avoid worrying about types if I want to be proficient at multiple dispatch though. Imagine you have a struct called Ball, and there are balls of different colors. I wanted to dispatch a different method for a Ball of different color. Initially, I was taking colors as a field, so I was creating one method that takes a ball and applies different operations based on if/then conditions inside that method. But then, if I can have Ball{Red}, Ball{Blue}, I can create methods specific to them. And, if I someday added a ball of a new color, I could just add a new method instead of mucking with the one giant method and risk breaking things for all Balls.
So I went against your advice and played with parametric types a bit and did this, which works as I expect it to:
julia> struct S2{T<:Type{<:Real}}
a::T
end
julia> S2{Type{Real}}(Real)
S2{Type{Real}}(Real)
julia> S2{Type{Complex}}(Complex)
ERROR: TypeError: in S2, in T, expected T<:(Type{<:Real}), got Type{Type{Complex}}
@DNF, thanks for clarifying. This is enlightening. What you showed is actually what I wanted. I started at it for 20 mins, and I think what you’re trying to tell me is that instead of doing this:
struct Ball{T<:Type{<:Color}} end
I should do this:
struct Ball{T<:Color} end
And then I can define a constructor that takes a type as a parameter if that’s what I want.
This makes a lot of sense.
=====
I am still confused with the ::, {}, and the where syntax. This is what I understand:
Your first constructor takes a type, a subtype of Color, as an argument. That’s what I presume the(::type) syntax does.
Your second constructor takes an instance of some Color, takes its type T, and creates an instance of Ball{T}.
The where statement is necessary if we want to use the T on the right hand side of the function definition (I think I read this somewhere).