Member-only story
On Invariants.
What is an invariant?
- The specification of a program should be its class invariants
- Aim to write programs so that you cannot create invalid objects or data.
I’ll use Rust as the language to describe the concepts behind this post, but try to make it accessible to those using other languages.
We can start with a simple object, such as a circle. A circle is completely defined by it’s radius. As long as our radius is positive, we have a valid circle:
#[derive(Debug)]
struct Circle {
radius: f64
}impl Circle {
fn new_option (radius: f64) -> Option<Circle> {
if radius > 0.0 {
Some(Circle {radius: radius})
}
else {
None
}
} fn new_assert (radius: f64) -> Circle {
assert!(radius > 0.0);
Circle {radius: radius}
}
}
For this example, we have two constructors: new_option
and new_assert
. They are two versions of the same constructor. The new_assert
function takes a radius, returns a circle if the radius is positive, and kills the program if it is not. The new_option
function takes a radius, returns a circle if the radius is positive, and returns nothing if it is not. Discussion on which method is more appropriate to use is left for another time.
It is impossible to create a circle with a negative radius if we can only use new_option
or new_assert
. Seeing as we have no functions available to modify a circle once we have created it, we can safely assume that every circle we encounter in our program will always have a positive radius. A positive radius is therefore an invariant of a circle.
We can extend our program to be able to modify a circle:
impl Circle {
// Constructors. fn grow (&mut self, length: f64) {
self.radius += length;
} fn shrink (mut self, length: f64) -> Option<Circle> {
if self.radius - length > 0.0 {
self.radius -= length;
Some(self)
}
else {
None
}
}
}
We create two functions grow
and shrink
. The grow
function increases the radius of a circle by length
and the…