Parameterizing type by integer value using "where"

This simple GaloisField type is parameterized by the modulus p and the integer type T. It works fine, but note that the test on the type of p buried within an inner constructor.

using Primes

# Type definition: GaloisField{p,T}, where p is prime modulus, T is integer type
struct GaloisField{p,T} <: Number where {p, T<:Integer}
    rep::T  # representative integer

    # inner constructor
    function GaloisField{p,T}(x::Integer) where {p, T<:Integer}
        if !(typeof(p) <: Integer) || !isprime(p)
            throw(ArgumentError("p must be a prime integer"))
        end
        return new(mod(x, p))
    end
end
GaloisField{p}(x::T) where {p,T<:Integer} = GaloisField{p,T}(x)

It would seem better to require that p be an integer in the type signature, like this

using Primes

# Type definition: GaloisField{p,T}, where p is prime modulus, T is integer type
struct GaloisField{p,T} <: Number where {p::Integer, T<:Integer}
    rep::T  # representative integer

    # inner constructor
    function GaloisField{p,T}(x::Integer) where {p::Integer, T<:Integer}
        if !isprime(p)
            throw(ArgumentError("p must be a prime"))
        end
        return new(mod(x, p))
    end
end
GaloisField{p}(x::T) where {p::Integer,T<:Integer} = GaloisField{p,T}(x)

but that gives the error

syntax: invalid variable expression in "where" around In[2]:4

Stacktrace:
 [1] top-level scope
   @ In[2]:4
 [2] eval
   @ ./boot.jl:360 [inlined]
 [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
   @ Base ./loading.jl:1094

Wouldn’t this be better? Why doesn’t it work?

And what’s with the syntax const T2 = Array{Array{T, 1}, 1} where T (from the UnionAll section of the Types in Julia Docs)? Am I supposed to read that as ...where T<:Any? To me an unadorned where T is like an unfinished sentence.

The GaloisField code is an update of an example from a 2016ish talk by Andreas Noack.

1 Like

I think you want to write where {p<:Integer, T<:Integer} since Integer is an abstract type.

And what’s with the syntax const T2 = Array{Array{T, 1}, 1} where T (from the UnionAll section of the Types in Julia Docs)? Am I supposed to read that as ...where T<:Any ? To me an unadorned where T is like an unfinished sentence.

I usually read it as “where T is some type” which seems to be the same as being a subtype of Any, but I don’t really know so maybe there could be something more intricate going on.

You cannot constrain the type of non-type parameter with where. Only type parameters can be constrained with where.

T is a type and the constraint T<:Integer signifies T must be a subtype of Integer
while p is a value of type Integer but this kind of constraint (type of value is ) cannot be enforced by where.

See Non-type as type parameters · Issue #9580 · JuliaLang/julia · GitHub

1 Like

Sorry, I should have clarified that the type is parameterized by the integer value of p. E.g.

julia> typeof(GaloisField{3,Int64}(1))

GaloisField{3, Int64}

This reflects the mathematics: the GaloisField with modulus p is a number system with p elements. Arithmetic is defined between elements of the same GaloisField, but, for example, you can’t add an element of a GaloisField with modulus 3 to an element of a GaloisField with modulus 5. So the type is parameterized by the value of the integer, as well as the integer type.

the integer 3 you see is not <:Ineteger:

julia> Array{Any, 3} <: Array{<:Any, <:Any}
true

julia> Array{Any, 3} <: Array{<:Any, <:Integer}
false

julia> Array{Any, Int64} <: Array{<:Any, <:Integer}
true

<:Integer means Int64, Int32 etc…

Yes, that’s why I want to specify where{p::Integer,T<:Integer} and not where{p<:Integer,T<:Integer}.

Letting go of some explicit Integer constraints, internalizing validations:


julia> 
struct GaloisField{p,T} <: Number
   rep::T  # representative integer
   # inner constructor
   function GaloisField{p,T}(x::Integer) where {p,T}
     if !isprime(p)
       throw(ArgumentError("p must be a prime"))
     elseif !(T <: Integer)
       throw(ArgumentError("T must be <:Integer"))
     end
     return new{p,T}(mod(T(x), p))
   end
 end

julia> GaloisField{11,Int32}(8)
GaloisField{11, Int32}(8)

or along the same lines, cleaner imo:

# this inner constructor is not called directly
struct GaloisField{p,T} <: Number
  rep::T  # representative integer
  # inner constructor
  function GaloisField{p,T}(x::Integer) where {p,T}
    return new{p,T}(mod(T(x), T(p)))
  end
end

# this becomes the interface
function GaloisField(p::T, x::T) where {T<:Integer}
  if !isprime(p)
    throw(ArgumentError("p must be a prime"))
  end
  return GaloisField{p, T}(x)
end
1 Like

Yes, you can do this, but relying on internal, user-defined type-checking seems harder to read & understand and more error-prone than declaring type constraints up front and letting the compiler enforce them.

Ah, I like your “cleaner imo” solution, having the user interface be function GaloisField(p::T, x::T) where {T<:Integer}.