Restricted parametric type

hi, how could I define a parametric type where only a subset of types are allowed? e.g.

struct MyType{S, T}

how could I put some arbitrary restrictions like:

MyType{Float64, Int64}(123)
MyType{Int64, Float64}(123)

are valid, and

MyType{Float64, Float64}(123)
MyType{Int64, Int64}(123)

are invalid?


1 Like

There are two approaches to constraining a struct’s field’s types. One involves constraining struct parameters and using those parameters for field types. The other involves constraining the types that struct constructors accept. They may be used separately or together.

If you have two fields and one should be a floating point value and the other should be an integer value:

struct Demo{A<:AbstractFloat, B<:Integer}

Demo(12.0, 5) # works
Demo(12, 5) # does not work

If you want one field to hold a Float64 value and the other field to hold an Int64 value, and sometimes you want the first field to be one type and other times you want the first field to be the other type

const IntFloat64 = Union{Int64, Float64}

struct Demo{A, B}

    function Demo(field1::A, field2::B) where {A<:IntFloat64, B<:IntFloat64}
        # ensure the types differ
        if A == B
           throw(ErrorException("types must differ"))
        return new{A,B}(field1, field2)

When the constructor is inside the struct (an inner constructor), and there are no outer constructors defined, then all construction will use the inner constructor.


It does not look like he wants to restrict the structs field types.

struct MyType{S, T}

function MyType(::Type{S}, ::Type{T}, content::Int) where {S, T}
    constraintypes(S, T)
    return MyType{S, T}(content)

function constraintypes(::Type{S}, ::Type{T}) where {S, T}
      s = supertype(S)
      t = supertype(T)
      if s<:t || t<:s
          throw(ErrorException("types must differ more"))
     return nothing
julia>MyType(Int, Float64, 5)

julia> MyType(Float32, Int16, 5)

julia> MyType(Float32, Float64, 5)
ERROR: types must differ more

oic: the trick is to use ::Type{} ?

actually I was looking into the code of StaticArray. Seems like it does not restrict the definition of nonsense types, e.g.:

julia> SMatrix{2, 3}
SArray{Tuple{2,3},T,2,L} where L where T

julia> SMatrix{-1, 3}
SArray{Tuple{-1,3},T,2,L} where L where T

noted that the 2nd type throws no error nor warning although it’s nonsense (to have negative dimension).

julia> SMatrix{2, 3}(11.0, 12.0, 13.0, 14.0, 15.0, 16.0)
2Γ—3 SArray{Tuple{2,3},Float64,2,6}:
 11.0  13.0  15.0
 12.0  14.0  16.0

julia> SMatrix{-2, 3}(11.0, 12.0, 13.0, 14.0, 15.0, 16.0)
ERROR: ArgumentError: Size mismatch in Static Array parameters. Got size Tuple{-2,3}, dimension 2 and length 6.

error is given only when calling constructors.

I understand your perspective and have no quarrel with it. We do it like this a lot:

When used, argument validation tends to be encapsulated in a validation function
or given as an inline test: arg1 > 0 || throw(DomainError("$arg1")).

Type signatures are used to guide multidispatch and also to gate arg types.

Mostly, Julia code is written to the type system rather than to defend it.

1 Like

inspecting SMatrix gives me a big surprise (due to my ignorance): the β€œparameter” of a parametric type can be anything, i.e. no need to be a Type !!!

struct MyStruct1{T}

function f(x::MyStruct1{T}) where {T}
    println("T is ", T, "   content is: ", x.content)

julia> f(MyStruct1{1}(123) )
T is 1   content is: 123

julia> f(MyStruct1{2.2}(222) )
T is 2.2   content is: 222

julia> f(MyStruct1{:sym}(333) )
T is sym   content is: 333

julia> typeof(MyStruct1{1}(123) )

julia> typeof(MyStruct1{2.2}(222) )

julia> typeof(MyStruct1{:sym}(333) )

so, I do not understand why we need Val{} at all???

Not quite, fourth bullet point in Types Β· The Julia Language.

For convenience! Of course, you can define your own Val-like type, but if you only want to dispatch on values, nothing more, someone already defined the type for you.

if you only want to dispatch on values, nothing more

I don’t understand. For example, the following use the value directly to dispatch and it’s all fine:

struct MyStruct1{T}

function f1(x::MyStruct1{1})
    println("T is 1")

function f1(x::MyStruct1{2})
    println("T is 2")

julia> f1(MyStruct1{1}(123) )
T is 1

julia> f1(MyStruct1{2}(222) )
T is 2

could you give an example like Val{1234} is necessary rather then the value 1234?

It is mostly for type stability. Some types have meaningful numbers in their parameters for example NTuple{N, T} where N is the number of elements in the tuple. Let’s say I want to generate 5 3-tuples and perhaps return a Scatter struct with points field.

julia> struct Scatter{N, T}
           points::Vector{NTuple{N, T}}

julia> Scatter(N, n) = Scatter([ntuple(i->rand(), N) for i in 1:n])

julia> Scatter(::Val{N}, n) where {N} = Scatter([ntuple(i->rand(), N) for i in 1:n])

julia> Scatter(3, 5)
Scatter{3,Float64}(Tuple{Float64,Float64,Float64}[(0.599768, 0.863335, 0.0522125), (0.461746, 0.991876, 0.349595), (0.976401, 0.146929, 0.393038), (0.570525, 0.752721, 0.00339558), (0.0793879, 0.612754, 0.00491688)])

julia> @code_warntype Scatter(3, 5)
Body::Scatter{_1,_2} where _2 where _1
1 1 ─ %1 = %new(getfield(Main, Symbol("##3#5")){Int64}, N)::getfield(Main, Symbol("##3#5")){Int64}            β”‚
  β”‚   %2 = (Base.sle_int)(1, n)::Bool                                                                         β”‚β•»β•·β•·β•· Colon
  β”‚        (Base.sub_int)(n, 1)                                                                               β”‚β”‚β•»    Type
  β”‚   %4 = (Base.ifelse)(%2, n, 0)::Int64                                                                     │││┃    unitrange_last
  β”‚   %5 = %new(UnitRange{Int64}, 1, %4)::UnitRange{Int64}                                                    β”‚β”‚β”‚
  β”‚   %6 = %new(Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##3#5")){Int64}}, %1, %5)::Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##3#5")){Int64}}
  β”‚   %7 = invoke Base.collect(%6::Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##3#5")){Int64}})::Array{_1,1} where _1
  β”‚   %8 = (Main.Scatter)(%7)::Scatter{_1,_2} where _2 where _1                                               β”‚
  └──      return %8                                                                                          β”‚

julia> Scatter(Val{3}(), 5)
Scatter{3,Float64}(Tuple{Float64,Float64,Float64}[(0.414659, 0.267734, 0.0170115), (0.763122, 0.858378, 0.45464), (0.0388137, 0.489361, 0.0495321), (0.0409206, 0.00572096, 0.924081), (0.350058, 0.133318, 0.890979)])

julia> @code_warntype Scatter(Val{3}(), 5)
1 1 ─ %1 = (Base.sle_int)(1, n)::Bool                                                                        β”‚β•»β•·β•·β•·β•· Colon
  β”‚        (Base.sub_int)(n, 1)                                                                              β”‚β”‚β•»     Type
  β”‚   %3 = (Base.ifelse)(%1, n, 0)::Int64                                                                    │││┃     unitrange_last
  β”‚   %4 = %new(UnitRange{Int64}, 1, %3)::UnitRange{Int64}                                                   β”‚β”‚β”‚
  β”‚   %5 = %new(Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##7#9")){3}}, getfield(Main, Symbol("##7#9")){3}(), %4)::Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##7#9")){3}}
  β”‚   %6 = invoke Base.collect(%5::Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##7#9")){3}})::Array{Tuple{Float64,Float64,Float64},1}
  β”‚   %7 = %new(Scatter{3,Float64}, %6)::Scatter{3,Float64}                                                  β”‚β”‚β•»     Type
  └──      return %7                                                                                         β”‚

If this code is in a hot part of the program, this type instability will propagate and slow down your whole program.



here I finally found the documentation also. It’s kind of difficult to understand though :sweat_smile: