Constraining Dart’s numeric types and why you may want to do this

Steve Hamblett
Level Up Coding
Published in
5 min readMar 16, 2023

--

I use several languages for my development work, as most people do, and I often see features in these languages that I wish Dart had. The Ada language is a (very)strongly typed language and has a particularly simple yet powerful feature I have used extensively over the years, that being subtypes.

A subtype in Ada is ‘a type together with an associated constraint’, i.e. a subset of the values of an existing type. An example -

subtype Small_Int is Integer range -10 .. 10;

This declares a type named Small_Int that is a subtype of Integer with the same operations as Integer but constrained to the value range -10 to 10. Any attempt to assign a value to this type that is outside the range of the constraint will cause the Ada runtime to raise a constraint error(a form of exception) that can’t be ignored, only handled. The type is used to best reflect the values to be stored. So what benefits does this give us -

  • Any person reading code will have better understanding of the purpose of a variable
  • Better error checking possibilities
  • Easier to prove properties of programs with more restricted types

A concrete example of this is converting data from the outside world into types in your application, e.g. deserializing a byte stream from a socket into a structure or record type. A subtype will not allow any value outside its constraints to be set, either at the point of deserialization or anywhere else in the application. This is important in high integrity/safety critical systems, where the propagation of invalid values into the application as a whole could have dire consequences.

So, can we do this in Dart? Well yes, we can’t enforce this at runtime in the same way Ada does this, but Dart is flexible enough so that with a combination of class extension and operator overloading we can model a working subtyping framework. For the purposes of this article, only ints are considered, however, the code presented below is easily made generic.
The full code for this article and unit test suite can be found here.

Firstly, create an abstract subtype class that more concrete types can be derived from -

abstract class Subtype {  
Subtype.set(this.start, this.end, {int initialValue = 0}) {
_value = initialValue;
}
final int end;
final int start;
int _value = 0;
int get value => _value;
void call(int val) => _value = _boundsCheck(val);

@override
String toString() => _value.toString();
...........

This allows us to set constraints for our subtypes and contains the private method boundsCheck to check these constraints. If the check fails, an ArgumentError is raised. Now we can add the equality operator and the + operator -

@override  
bool operator ==(Object other) {
if (other is int) {
if (_value == other) {
return true;
}
}
if (other is Subtype) {
if (_value == other.value) {
return true;
}
}
return false;
}

dynamic operator +(Object other) {
if (other is int) {
_boundsCheck(_value += other);
} else {
try {
_boundsCheck(_value += (other as Subtype).value);
} on ArgumentError {
rethrow;
} catch (e) {
throw ArgumentError(
'AdaTypes:Subtype - the type supplied is not derived from Subtype - $e',
'other');
}
}
return this;
}

For equality, we allow comparison to ints as well as other Subtypes, similarly for addition we allow addition of ints and other types derived from Subtype (this can be further constrained, see below). Note, the return type of the addition operator is dynamic to allow default operation for derived types. So how can we use this? One way is to create general purpose Subtypes and use type aliasing to aid readability -

class ADozen extends Subtype {  
ADozen() : super.set(0, 12);
}

class AScore extends Subtype {
AScore() : super.set(0, 20);
}

typedef EggsInBox = ADozen;
typedef PeasInPod = AScore;

This allows us to write our subtyped integers like this -

final numEggs1 = EggsInBox();  
numEggs1(3);
final numEggs2 = EggsInBox();
numEggs2(4);
final numPeas = PeasInPod();
numPeas(15);

Addition now simply becomes -

numEggs1 += numEggs2;

knowing that the subtype constraints cannot be violated. All other needed operators can be added similarly.

So far so good, but the implementation thus far also allows this -

numEggs += numPeas;

as both the types are derived from Subtype. This doesn’t read naturally and is probably not what you expect, so we can tighten this further by adding a strict type checking mechanism and while we are at it a check that the variable can’t be used unless it has been initialized -

Subtype.set(this.start, this.end,  
{int initialValue = 0, strictTyping = false}) {
_value = initialValue;
_strictTyping = strictTyping;
}

final int end;
final int start;
int _value = 0;
int get value => _value;
bool _strictTyping = false;
bool _initialised = false;

so the addition operator now becomes -

dynamic operator +(Object other) {  
if (other is int) {
_operationValid(_value += other);
} else if (_strictTyping) {
final tThis = runtimeType;
final tOther = other.runtimeType;
if (tThis != tOther) {
throw ArgumentError(
'AdaTypes:Subtype - strict typing - the type supplied is different from the subject type',
'other');
} else {
_operationValid(_value += (other as Subtype).value);
}
} else {
_tryAssignment(other);
}
return this;
}

the addition of peas in a pod to eggs in a box above will now throw as the types ADozen and AScore if instantiated with the strict typing flag, although still derived from Subtype are now treated as different types. Type aliasing doesn’t have to be used, of course, there’s nothing to stop the user directly creating an EggsInABox type directly from Subtype.

As can be seen, creating a fully fleshed out production ready subtyping mechanism with a corresponding unit test suite is not trivial and will not suit everyone. However, once implemented, this can be re-used in many of your packages and applications.

The real take out here though is not just the code, it’s instilling a questioning mindset in the eyes of the reader. If you now look at your code differently and ask questions like, ‘Does that variable really need the full range of an int? In this application I know it can only be 0…10, what happens if it gets assigned the value 20 or a negative value?’ Then you are on your way to producing even more type safe code than standard Dart allows.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--