Parametric types - type definition in struct name or struct argument?

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

Case 2 is the easier one to explain:

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

struct SS
    a::Int64
end
1 Like

Case 1 is a bit trickier, I suggest you read the docs on type selectors first. What are you trying to achieve exactly?

I’m not 100% sure what’s happening here, but maybe you want

struct S{T<:Real}
    a::Type{T} 
end

?

2 Likes

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)

but really, like Types · The Julia Language says,

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.

1 Like

@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}}

The idea wasn’t that you should not worry about types, but that you shouldn’t worry about Type.

What about

abstract type Color end
struct Red <: Color end
struct Ball{T<:Color} end
Ball(::Type{T}) where {T<:Color} = Ball{T}()

Or, instead of the last constructor, a popular pattern these days are

Ball(::T) where {T<:Color} = Ball{T}()

With the first constructor, you do

julia> Ball(Red)
Ball{Red}()

while with the second, you do

julia> Ball(Red())
Ball{Red}()

I don’t really see why you need the Type as part of the type parameter.

3 Likes

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

Exactly. And then you can decide whether to define a constructor for Ball(Red) or Ball(Red()).

Yes to both.

Yes, you use this syntax in order to capture the type in a variable, T. But you could also in stead do

Ball(c::Color) = Ball{typeof(c)}()

and then Ball(Red()).