Restricted Types

A restricted type is one where there are one or more restrictions on the valid instances of that type. An example of a restricted type is an Integer which is restricted to have values from 1 to 10 inclusive. Restricted types are another form of generics and are defined just like generic types. For example, here is how such a range-restricted Integer type would be defined in Jura:

  doc={"A number which is restricted to be in a given range of values."}

  Property:min{type=Long doc={"The minimum valid value of an instance of this type"}}
  Property:max{type=Long doc={"The maximum valid value of an instance of this type"}}
  Property:base{type=IntegerPrimitive doc={"The base type whose range is being restricted"}}

    doc={"This creates a primitive which extends and restricts the base type."}

      doc={"Checks to ensure that the supplied instance is in range."}
      If{test=((i<$min) || (i>$max)) //
          new InvalidValueException{msg="Value is not in the range "+$min+".."+$max}

An integer field whose values are restricted to be in the range 1 to 10 can then be defined using

Field:x{type=Range{base=Integer min=1 max=10}}

Now suppose we wish to assign an instance of an unrestricted integer to x.

x=y           {* This will fail at compile time *}
x=(x.TYPE)y   {* A cast is required from an unrestricted integer to a restricted integer. *}
y=x           {* This is always allowed. *} 

In this case, y is 7 and the cast will succeed.  If y was 11 then the cast would fail and an InvalidValueException would be thrown.

Overriding isAssignableTo() and isCastableTo()

To make using restricted types more intuitive, they should override the default implementations of isAssignableTo() and isCastableTo().  In the Range example above, isAssignableTo() should be overridden to return true for any Range instance which is a sub-range of the current instance.  Then statements like:

Local:x{type=Range{base=Integer min=1 max=10}}
Local:y{type=Range{base=Integer min=1 max=100}}
y=x {* isAssignableTo() should be overridden so that this will succeed at compile-time *}

Note: It follows that any method on a class that implements Type may be called by the compiler.  Such methods should therefore ensure that they execute quickly and do not use excessive system resources.  For security, the compiler should execute such methods in a sandbox preventing all access to external resources (e.g. file system, network etc).  

Using restrictions to create more meaningful primitive types

Given that we can now restrict the values of primitive types, it makes sense to define more meaningful primitives with predefined restrictions.  For example, we could define a percentage type whose value is limited to be between 0 and 100:

IntegerPrimitive:Percentage{extends=Range{base=Integer min=0 max=100}}

We can then define a field like:


The field score will then behave as follows:

score=110 {* This will fail at compile time. *}
score=(Percentage)110 {* This will succeed at compile time but always throw an exception at run time *}
score=(Percentage)70  {* This will succeed at both compile time and run time *}

The restricted type should define restricted return types for operations on that type e.g. arithmetic operations.  If this is not done, then any arithmetic operation on a type will have return a value whose type is unrestricted.  For example, using the basic definition of Range above, then the expression score+1 will have type Integer.  Thus, by default, the expression score=score+1 will fail at compile time and score=(Percentage)(score+1) would have to be used instead.  Of course, if score had an initial value of 100 then the cast would cause an exception to be thrown.

Note: In Jura, the use of direct casts is discouraged in favour of the IfCast statement as this will never cause an exception to be thrown.

A more complete definition of Range would provide restricted return types for all arithmetic operations by overriding and restricting the return types of appropriate methods of IntegerPrimitive