Defining new types with strict requirements

Hi!

I intend to create a set of dozens of Julia functions routines that will operate on objects a, b, c and d, where the following conditions must always be verified:

  • a is an 8-bit unsigned integer such that 1 < a < 250;
    AND a can be incremented or decremented by arbitrary amounts, as long as the new value remains within that range;
    AND operations that would generate a result outside of that range warp the result around: for instance, if a = 199 then a+1 = 1.

  • b is a 32-bit unsigned integer such that 500 < b < 200,000;
    AND b can be incremented or decremented by arbitrary amounts, as long as the new value remains within that range (otherwise, throw an error).

  • c is a 16-bit unsigned integer within the set {4, 50, 300 or 2400}, but no other value is allowed, and no arithmetic operation is meaningful on that object.

  • d is a string that must be either “d1”, “d2”, or “d3”.

I want to avoid checking the validity of each input argument at the start of each function and would rather rely on defining custom types for those four objects, so that once they have been properly defined and initialised, they can be safely manipulated by the functions (which would require those inputs to be of the right type). How could I achieve this?

Thanks for your help, Michel.

Please see the Julia doc Constructors · The Julia Language. Inner constructor will disable the default constructor. You can then enforce your restrictions inside this constructor.

1 Like
struct A <: Integer
    a::UInt8
    function A(a)
        a = mod(a, 250)
        return new(a)
    end
end
struct B <: Integer
    b::UInt32
    function B(b)
        (500<b<200_000) || error("No!")
        return new(b)
    end
end
struct D <: AbstractString
    d::UInt8
end
string(d::D) = "d$(D.d)" 

That’ll get you started.

3 Likes

In fact, you don’t need to define string(::D). Instead, you need four Base functions:

import Base: ncodeunits, codeunit, isvalid, iterate
ncodeunits(::D) = 2
function codeunit(d::D, i)
    i == 1 && return 0x64
    i == 2 && return 0x30 + d.d
    throw(BoundsError("No!"))
end
isvalid(d::D, i) = 0<i<3
function iterate(d::D, i::Integer=1)
    i == 1 && return ('d', 2)
    i == 2 && return (Char(0x30+d.d), 3)
    return nothing
end

Thank you both, liuyxpp and gustaphe, for your inputs: I’m making progress…

After playing around with those concepts, I think it makes sense to include checks on the validity of input variables within the definition of custom-designed types, though verification of the validity of operations on those variables should not be part of the type definition.

The type definition for the first case mentioned in the original message now looks like

julia> struct A <: Integer
           a::UInt8
           function A(a)
               if a < 1
                   return "Argument can't be smaller than 1."
               end
               return new(a)
           end
       end

julia> 

julia> a1 = A(3)
A(0x03)

julia> a2 = A(a1.a - 3)
"Argument can't be smaller than 1."

julia> a2 = A(a1.a + 3)
A(0x06)