Recently I listened to a “Happy Path Programming” podcast episode about Smart Types. And that inspired me for this double post. The first part (this one) is about what a smart type is and why you should employ smart types in your code. The second part (yet to come, hopefully soon) is about the troublesome way I implemented an arithmetic smart type template in C++.
At the dawn of the computer age, it was just bits and bytes, wire and silicon (or relays and vacuum tubes) containers for ones and zeros. The need for abstraction led the early programmers to define different kinds of data: boolean, integers, floating-point numbers, strings. These types were a huge step forward because let engineers handle things they were used to handling – arithmetics, and symbols.
This brought an interesting practical property – while no one prevents you to combine bits with bits, regardless of their meaning, you may feel uneasy to sum integers with booleans. By trusting language and compiler, the programmer could get an extra review of the code she/he wrote, finding subtle mistakes about how values are handled and passed around.
For an extremely long period of time (relatively speaking), programmers cheerfully used language built-in types to encode a large set of information; happy with some feedback from the compiler when they tried to pass an integer when a pointer to char was expected.
So far so good, isn’t it?
Now let’s consider the following API –
void newWindow( int width, int height, bool showInFront );
Well… It may make sense, but let’s take a closer look.
Let’s start from the showInFront
parameter. The author declared it as a boolean, only because the new window can be either in front of the other windows or not. But besides the fact that it can have only two values (in front/not in front), there is no other relationship with the concept of bool. In other words, using True to say that the window is in front and False to say that the window is behind is just a convention. In fact, when you read the API call like:
newWindow( 320, 200, true );
You can’t figure out what the third argument is until you look up the API definition to read the parameter name. The only advantage of using a boolean type is to avoid the chance of using a value of range.
A better approach would be to use an enum
(even better an enum class
) to give a better name to the type and to improve the readability.
And now let’s focus on the first two arguments. If you have spent some time on first-gen PC hardware, then you will recognize a standard resolution, and either from this or from your analytic geometry class you know that the first number is the horizontal value and next, there is the vertical value. But if you just finished handling matrices you may be tempted to consider the first argument as the number of pixel rows (vertical resolution) and then the number of pixel columns (horizontal resolution).
Some languages (actually quite a lot, but C++) offer a “named parameter” convention that you can use to partially solve this ambiguity:
newWindow( width=640, height=480 );
Is there a way to avoid the confusion and let the compiler catch this kind of mistake? (spoiler – yes)
The solution is simple (once you know it) and straightforward. Push the concept of types a bit further. In fact, nothing prevents me from defining a specialized int to represent only horizontal resolution. If I carefully avoid implicit conversions, I can be sure that I can’t accidentally pass a Width where a Height is expected. This new API would be written like:
void newWindow( Width width, Height height );
One notable aspect is that the name of the type matches the name of the argument and this is the ultimate goal – no two parameters may have the same type unless you can safely exchange one with the other (e.g. min
and max
functions).
Smart types bring additional benefits. Consider the Speed type. Since you control the creation and the access to the value, you can no longer misinterpret the measuring unit – you could design your API so that the name of the factory method clears any doubt about the interpretation of the dumb type:
class Speed
{
public:
static [[nodiscard]] Speed fromKilometersPerHour( float kph ) noexcept;
static [[nodiscard]] Speed fromMetersPerSecond( float mps ) noexcept;
[[nodiscard]] float asKilometersPerHour() const noexcept;
[[nodiscard]] float asMetersPerSecond() const noexcept;
private:
explicit Speed( float theClassKnowsWhat ) noexcept;
};
This may resemble the dimensional analysis, but is way simpler to implement and is part of a general approach to types.
Another brilliant application is data validation. When you get strings from an external input, it is a good practice to validate them in order to prevent malicious (or clumsy) content from being interpreted (as in the infamous SQL injection). Adding the smart type ValidatedString
the can contain only validated strings, then you avoid the problem entirely because
- It is clear if a function accepts any string or needs a validated string
- an unvalidated string cannot be mistakenly passed to a function that doesn’t properly handle it.
Eventually, I leave you with one last example to remark on the power of smart types – Division. The function division is defined for every pair of numbers (a,b), but when b is 0. Historically two approaches have been used when the divider is 0 –
- someone else problem approach, i.e. raise some form of exception;
- garbage in – garbage out approach, i.e. you’ll get an invalid response in the form of NaN.
Both are not really solutions to the problem and in both cases, error handling is a pain in the neck, to the point that often the programmer tends to ignore the problem.
From the functional programming land a third approach is available – wrap the result in an Option. If the division is defined then the Option contains the result, otherwise it contains nothing. Although elegant, managing this kind of result is not effortless. In order to get some result, you should move everything into a flatMap or in a for comprehension – which is a construct not (yet) available in C++.
Enter Smart Types – just define a NonZeroNumber
type to use as a dividend and you’ll be unable to express an invalid division. And that’s not error-recovering at run-time, but error-prevention at compile time, which will cost zero during the execution.