Expressive Code

How to Set A Maximum or Minimum Value with Expressive Code

Illustrated by Leandro Lassmar

There is something inherently confusing with the typical code to set a maximum or minimum value, in C++ as well as in other languages.

Imagine, for example, a heating system that can't accept a thermostat of more 30 degrees Celsius, regardless of the temperature its user asks for. The typical code to express that bounded value looks like this:

auto const temperature = std::min(inputTemperature, 30);

We want to set a maximum for the value, but the code reads "minimum."

It makes sense when you think about it. To make sure the value doesn't go above 30, we take the smallest value between the required temperature and 30. If temperature is smaller than 30, we take it as it is. If the temperature exceeds 30, we use 30 which is then the smaller of the two values.

This code works, but the problem is that for it to make sense, we have to think about it. And to express such a simple idea as setting a maximum value, a line of code should not make us think. However, I often encounter this sort of code, and I have written it several times too.

What follows is a possible solution to improve this code, relying on strong types. Rather than a definitive solution, this is perhaps a basis for discussion.

Expressing the Intent in Code

One way to write expressive code is to start by deciding what the code should look like without thinking about language constraints, and then using the tools of the language to get as close to that target as possible.

So what would we like the initial code that sets a maximum to look like?

Let's start by putting words to the concept of setting a minimum or maximum value. In finance, to set a maximum is to cap a value and to set a minimum is to floor it. Even though std::floor does a slightly different thing, we'll use those terms in our examples.

An Infix Syntax?

In the example of the heating system above, we want to cap the temperature at 30 degrees Celsius. A good target for code could therefore be:

auto const temperature = inputTemperature capped at 30;

Of course this is not compiling C++ code. But this will do for our target.

The main reason why this is not C++ code is that C++ doesn't have an infix notation for functions, that is to say that we can't call a function by placing it between its arguments. To invoke a function f on x and y in C++ we have to write:

f(x, y);

There is no way around it. In particular, we can't write something like this:

x f y;

It's interesting to note that some languages offer this possibility. In Haskell for example, any function can be called with an infix syntax by using backquotes:

x `f` y

So in Haskell we could have written a function cappedAt that would have allowed us to write code like this:

temperature `cappedAt` 30

This would have been nice. But it's not possible in C++, nor is it possible in Java, C#, and many other languages as far as I know.

We could imagine adding a cappedAt method to the type of temperature to be able to write this:

auto temperature = inputTemperature.cappedAt(30);

But there are several arguments against doing this:

  • the code of capping a value should not be specific to temperature, and therefore should not be in its type (just like std::min is not). We could to resorting to the CRTP in this case, but it seems overkill.
  • if temperature is of type int, we cannot add any method to it to begin with.

A Basic Solution

Since we have to work with a prefix notation like f(x, y), the simplest solution is to go for a simple function:

template<typename T>
T const& cap(T const& value, T const& maximumValue)
{
    return std::min(value, maximumValue);
}

We can call it this way:

auto const temperature = cap(inputTemperature, 30);

In this example, it looks clear which parameter is the bound: 30. But if we pass to cap two named objects, it can be a little more confusing:

auto const temperature = cap(inputTemperature, adminTemperature);

Someone discovering this code will have to guess which parameter is the bound. And we're back to the initial problem: reading this code would require its reader to think. And such a simple concept should not slow us down when skimming through code. A reader may even end up wasting time looking up the definition of the function to be sure of which parameter is the bound.

As a little aside, you may wonder why I am so stingy regarding time reading code.

It is because being able to read code quickly and understand most of it is an invaluable skill in our industry. We spend most of our time reading code, or at least we spend way more time reading code than writing it. Being a code speed-reader allows you to get a better grasp of existing code, which can be a life-changer in many of your tasks.

If we have to think or go look up the function definition for each line of code including when it is merely setting a maximum value, skimming through code quickly turns into trudging through it. This is why we need to make it instantly clear which parameter is the bound.

Using Strong Types

Strong types are a great way to clarify the roles of the arguments passed to a function. We will use NamedType as an implementation of strong typing in C++, and if you're not familiar with strong types, you can get up to speed by checking out strong types for strong interfaces.

What name should we use for the type of the bound? We could use Bound, but going back to our target syntax:

auto const temperature = inputTemperature capped at 30;

calling it "at" would get us closer. Since cap is a template function, let's create a template strong type for at:

template<typename T>
using At = NamedType<T const&, struct atTag>;


template<typename T>
At<T const&> at(T const& value)
{
    return At<T>(value);
}

We can now adapt cap to accept an at:

template<typename T>
T const& cap(T const& value, At<T const&> maximumValue)
{
    return std::min(value, maximumValue.get());
}

This leads to the following call site:

auto const temperature = cap(inputTemperature, at(30));

or

auto const temperature = cap(inputTemperature, at(adminTemperature));

This reads more like natural English and makes it obvious that the second parameter is the upper bound.

Symmetrically, to floor a value to a minimum, the code would be:

template<typename T>
T const& floor(T const& value, At<T const&> maximumValue)
{
    return std::max(value, maximumValue.get());
}

With the following call site:

auto const temperature = floor(inputTemperature, at(30));

How does this look to you?

Did you also find the code using std::min to set a maximum value confusing? Do you find the version with strong types clearer, or more complicated? Do you have other ways to implement this simple concept in expressive code?

Jonathan Boccara

author

Jonathan Boccara is a C++ software engineering lead, blogger and writer focusing on how to make code expressive. His blog is Fluent C++.

Leandro Lassmar

illustrator

Leandro Lassmar is an illustrator living in Minas Gerais, Brazil.
He worked in animation studios, currently works for magazines, books and advertising.
It won the SND (society of news design) and ÑH - Lo Mejor del Diseño Periodístico awards.