Avoiding primitives

May 2018

A few years ago when I was moving out from my parent’s house I asked my dad for a life advice. He thought for a moment and said:

Learn from your mistakes son, but what is even better is to learn from other people’s mistakes.

So why I’m telling you all this? The history of software engineering is full of failures from which we should learn. This article is inspired by one of the most known software failures later called the metric mix-up. On September 23, 1999, the Mars Climate Orbiter was lost while entering orbit around Mars. The probe came too close to Mars as it tried to maneuver into orbit and is thought to have been destroyed by the planet’s atmosphere. An investigation said the root cause was that different parts of the engineering team were using different units of measurement. One group working on the thrusters measured in English units of pounds-force seconds and the others used metric Newton-seconds. In the result, the thrusters were 4.45 times more powerful than they should have been. It’s unbelievable, but such a bug happens often when primitive data types are used to represent domain ideas.

avoiding primitives

Primitive types are types built-in to the language. They are the building blocks to create custom types. Often, they are overused instead of defining domain-specific types. For instance, we use a string to represent a name, an integer to represent a distance, or a dictionary to represent an object. Here’s some example code.

struct User {
    let username: String
}

It looks fine, but we have to put a constraint that username should have at least 3 characters. Such constraints tend to leak across the code base. The leaking problems occur either by not handling well at some places or handling them wrongly or repeat these constraints across the code. Instead of a string, we should make a username type.

struct User {
    let username: Username
}

struct Username {
    private let value: String

    init?(value: String) {
        if value.count < 3 { return nil }

        self.value = value
    }
}

The string type can represent any text, but a username cannot be any text. This type can encapsulate all the constraints regarding username in a single place. Instead of using string type in other places, we start to re-use username type which takes care of its constraints. Let’s focus on next example which is a simple method to calculate speed.

func calculateSpeed(distance: Double, time: Double) -> Double {
    return distance/time
}

Without looking at the function and the parameters names, it can be quite confusing to find what those parameters represent. This increases the opportunity for bugs to sneak in. It’s not that uncommon to get the parameters wrong if the signature of a method relies on primitive types. What’s more, we don’t have any information about metric units here. Let’s add new types for distance, time and speed.

func calculateSpeed(distance: Distance, time: Time) -> Speed {
    return Speed(metersPerSecond: distance.meters / time.seconds)
}

struct Distance {
    let meters: Double
}

struct Time {
    let seconds: Double
}

struct Speed {
    let metersPerSecond: Double
}

Now the compiler helps us to protect the parameters from a mismatch. Parameters have associated value with a measurement unit. The code is more readable, it describes himself even without the function name.

Thanks for reading! I’d love to hear comments and feedback. Contact me on twitter @wojteklu. 👋🏻