Julia parametric subtyping "<:" operator equivalent for declaring the parametric type

420 Views Asked by At

This is me probably going off what is considered idiomatic in Julia, as I am clearly abusing parametric types (generic types instead of hardcoded ones) but I am struggling to understand why this doesn't work and what are my alternatives.

Say we have this parametric type {T}:

struct mystruct{T}
    val::T
end

I can restrict the possibilities by supertyping T:

struct mystruct{T<:Number}
    val::T
end

But I don't seem able to restrict it to a certain type:

struct mystruct{T::Int64}
    val::T
end

My goal is being able to declare several fields at once by declaring the generic struct with Int64 as I did above. So I'm looking for the <: equivalent.

My end goal is to do this:

struct mystruct{T::Int64}
    val::T
    max::T
    mystruct(val::T) = begin
    new(val, typemax(T))
    end
end

Instead of what I currently do:

struct mystruct
    val::Int64
    max::Int64
    mystruct(val::Int64) = begin
    new(val, typemax(val)) # or typemax(Int64)
    end
end
4

There are 4 best solutions below

8
DMeneses On

I just noticed that subtypes(Int64) returns nothing so in my case {T<:Int64} already does what I wanted.

For types which are not at the end of their hierarchy, an abstract type such as Signed, it wouldn't make sense to do what I am asking: {T::Signed}

So it's a non issue.

4
Max On

This sounds a bit like a solution in search of a problem—ultimately, doesn't your issue boil down to Int64 being too long to type?

If so, you could just define a type alias

const T = Int64

at the top of your code.

If T is different every time, you could use a let block like this:

julia> let T = Int64
           struct MyStruct
               a::T
               b::T
               c::T
           end
       end

julia> MyStruct(4, 5, 6)
MyStruct(4, 5, 6)

Then T is no longer in scope, which is probably what you want:

julia> T
ERROR: UndefVarError: T not defined

Finally, although I can't see a reason for it, you can also have a parametric struct whose parametric type is just Int64: struct MyStruct{Int64} a::Int64 end.

And note that T::Int64 and T<:Int64 aren't really analogous in the sense that t == 5 and t <= 5 are. The visual similarity is misleading: In T<:Int64, T is a type that is a subtype of Int64, whereas in T::Int64, T is a value of type Int64. To keep yourself on the right track, you should make sure that whatever you write before :: is in lowercase, which customarily denotes a value rather than a type.

3
Bill On

I don't see much utility in restricting parametric types to one type, but you can do so by restricting the type in your internal constructor:

julia> struct mystruct{T}
           val::T
           max::T
           mystruct(v::T) where T <: Int64 = new{T}(v, typemax(v))
       end

julia> mystruct(4)
mystruct{Int64}(4, 9223372036854775807)

julia> mystruct(n) = mystruct(Int64(n))
mystruct

julia> mystruct(Int32(5))
mystruct{Int64}(5, 9223372036854775807)
 
0
Bogumił Kamiński On

As you can read in the section on Parametric Composite Types of the Julia manual and the following section Julia allows you to define types that have parameters. Such types have names Type{TypeParameters}.

Note that, on purpose I concentrate only on type name not on whole type definition as it is irrelevant. What is important to understand here that the whole construct Type{TypeParameters} defines a type, i.e. the {TypeParameters} part is part of the type. Here is an example:

julia> struct Type{Int}
       end

julia> t = Type{Int}()
Type{Int64}()

julia> typeof(t)
Type{Int64}

Note that type parameter, in general does not have to be associated with fields of a composite type:

julia> struct Type1{T<:Int}
           val
       end

julia> Type1{Int}("a")
Type1{Int64}("a")

julia> Type1{Union{}}("b")
Type1{Union{}}("b")

The parameters of a type are just a way to bundle together multiple types sharing some common structure and functionality.

Now note:

julia> struct Type2{T<:Int}
           v::T
           Type2{T}() where {T} = new()
       end

julia> Type2{Int}()
Type2{Int64}(1609504000)

julia> Type2{Union{}}()
Type2{Union{}}(#undef)

julia> Type2{Any}()
ERROR: TypeError: in Type2, in T, expected T<:Int64, got Type{Any}

Here I used incomplete initialization to actually create an instance of Type2{Union{}} type that has a field of type Union{} although it is impossible to properly initialize it.

See:

julia> x = Type2{Union{}}()
Type2{Union{}}(#undef)

julia> fieldtypes(typeof(x))
(Union{},)

I additionally run Type2{Any}() constructor to show you that the type restriction of struct is enforced.

In summary - in general type parameter is just a part of the type name. Then when defining types writing Type{<:SomeType} is just Julia analogue of covariant type. Just as Type{>:SomeType} is analogue of contravariant type. See here for an explanation. Also maybe discussion of UnionAll types might be helpful for you to understand this topic.