Au 101: Quantity Makers¶
This tutorial gives a gentle introduction to the Au library.
- Time: TBD.
- Prerequisites: Experience writing C++ code.
- You will learn:
- The concept and importance of “unit safety”.
- How to store a numeric value in a quantity.
- How to retrieve the stored numeric value.
- Some basic operations you can perform with a quantity.
Status quo: no units library¶
Suppose you have a variable that represents a physical quantity. That variable has some value, but that value is meaningless unless you also know the unit of measurement. We usually indicate the unit with a suffix on the variable name. Here’s a concrete example:
const double track_length_m = 100.0;
// Unit suffix--^^ ^^^^^--Value
const double best_time_s = 10.34;
// Unit suffix--^^ ^^^^^--Value
The first value is 100.0
. Since there’s no such thing as a “length of 100”, we add a _m
suffix
on the end of our variable name to make it clear that the value is the length in meters. We take
a similar approach for our time in seconds.
This strategy works, in the sense that it can prevent unit errors, but it’s labor intensive and error prone. The naming suffixes provide hints, but enforcement is basically on the honor system. Consider a function we might want to call:
With the above variables, our callsite might look like this:
It’s time to consider a very important property:
Definition
Unit correctness: a program is unit-correct when every variable associated with physical units is used consistently with those units.
So: is this unit-correct? Yes:
track_length_m
gets passed as the parameterlength_m
: meters to metersbest_time_s
gets passed as the parametertime_s
: seconds to seconds
However, it’s quite fragile. We could just as easily have written the following.
By itself, this line looks correct: we’re asking for an average speed, given a time and a length. We can even see that we’re passing in values in seconds and meters to get a result in meters-per-second, increasing our confidence!
Of course, the line is wrong, but the only way to know that it’s wrong is to go read the
declaration of average_speed_mps
. This could easily be in some other file. In a big project, it
might be hard to even figure out which file it’s in.
That’s a lot of cognitive load!
Our goal: unit safety¶
To write code quickly and robustly, unit-correctness is not enough. We need more: we need unit safety.
Definition
Unit safety: We call a program unit-safe when the unit-correctness of each line of code can be checked by inspection, in isolation.
This is the way to reduce cognitive load for code readers, when it comes to physical units. If you inspect a unit-safe line, and see that it’s correct, then you’re done with that line. You can move on; you don’t have to hold it in your head.
Tip
A unit-safe line doesn’t guarantee that the program has no unit errors. It does guarantee that if there are unit errors, then they’re in some other line (which you can also inspect!).
Unit-safety is not something you could ever get from the standard numeric types, but you can get it from the Au library. Let’s learn how!
Storing values: the “quantity maker”¶
The way to achieve unit-safety is by turning our raw numeric values into quantities. We do this with quantity makers. These are callables—things that act like functions—which have the name of some unit, and accept any numeric type.
For example, let’s make our variable track_length_m
unit-safe by using the quantity maker,
meters
:
const auto track_length = meters(track_length_m);
// ^^^^^^ ^^
// Quantity maker of *meters* Takes value in *meters*
This is an example of a unit-safe handoff. We take a raw number whose name tells us it was in meters, and we pass it to the quantity maker for that same unit. We can see this line is unit-correct simply by inspection—our first example of a unit-safe line.
In fact, we have already achieved unit safety everywhere we use the quantity track_length
instead
of the raw number track_length_m
! Think of the quantity as a container, which holds its value
securely together with information about its unit. We’ll see that the quantity prevents us from
using that value in ways that are incompatible with its unit.
Retrieving values: you must name the unit¶
Ideally, every interface that takes physical quantities would use unit-safe quantity types. In practice, you can’t upgrade your entire codebase at once. Even if you could, there will always be third-party libraries which don’t know about these quantity types. One way or another, it’s important to be able to get the value out.
Let’s imagine we have this example third-party API, which needs a raw double
. How can we call it
if we have a quantity?
// Example third-party API.
class Racetrack;
class RacetrackBuilder {
public:
// Main function we'll call:
void set_length_m(double length_m);
Racetrack build_track();
};
Most units libraries provide a function that retrieves a quantity’s value “in whatever units it
happens to be stored”. (Think of
std::chrono::duration::count()
as a
very common example.) These kinds of functions may be convenient, but they’re not unit-safe.1
Au takes a different approach. To retrieve the value from a quantity q
, you call q.in(units)
,
where units
is the quantity maker you used to store the value. Continuing with our earlier
example, we could call that API like so:
RacetrackBuilder builder;
builder.set_length_m(track_length.in(meters));
// ^^ ^^^^^^^^^^^
// API wants length in *meters* Get value in *meters*
Here, we have another unit-safe handoff. Our first one showed how we enter the library by naming the unit. This one shows how we exit the library by naming that same unit.
Tip
Think of the quantity maker’s name as a kind of “password” which you set when you create the quantity. The quantity will hold its underlying value securely. To retrieve that value, you must speak the same “password” (that is, name the same unit).
Of course, this API is a best-case scenario for raw numeric APIs, since it names the units at the
callsite (via the _m
suffix on set_length_m()
). Our other API, average_speed_mps()
, can’t do
this, because we can’t see the parameter names at the callsite. In fact, although we’ll see some
coping strategies in later lessons, there is no unit-safe way to call average_speed_mps()
directly.
Basic quantity operations¶
Quantity types do much more than simply hold their values securely: they support a variety of operations. In fact, we strive to support every meaningful operation, because operation implementations for quantity types can faithfully maintain unit safety.
Tip
Treat any instance of retrieving the value as “code smell”. Stop and check whether there’s some way to perform the operation within the quantity type. If there’s not, stop and consider whether there should be.
By “code smell”, we don’t mean that it’s definitely wrong; in fact, it’s often necessary. We just mean it’s worth checking to see if there’s a better pattern.
The first and most basic operations which we’ll cover here are arithmetic operations.
- You can add, subtract, and compare quantities of the same units.2
- You can multiply and divide quantities of any units.
Example: same-unit operations
Here are a couple examples of operations among quantities with the same unit.
constexpr auto distance = meters(1.0) + meters(2.0);
// distance -> meters(3.0)
constexpr auto is_duration_greater = (seconds(60) > seconds(55));
// is_duration_greater -> true
Admittedly, these examples are very basic for now. Future lessons will explore more interesting examples—like, what happens when you compare a length in inches, to a length in centimeters? But for now, the takeaway is simply that we neither need nor want to extract underlying values to perform basic operations.
Multiplying and dividing quantities¶
The product of two quantities is another quantity.
Recall that a quantity variable has two parts: the unit, and the value. These parts compose nicely with multiplication.
- The unit of the product is the product of the units.
- The value of the product is the product of the values.
All of these same considerations apply to division.
So for example: (meters / second)
is a quantity maker. You can call it and pass any numerical
type, just as with the quantity makers meters
or seconds
. In particular,
Tip
To form a compound quantity maker, use the grammatically correct name of the unit. Examples:
meters / second
, notmeters / seconds
newton * meters
, notnewtons * meters
Empirically, we have found that this pattern works: (s * ...) * p / (s * ...)
. That is:
- pluralize only one token
- for singular tokens: put those which multiply on the left, and those which divide on the right.
Exercise: computing with quantities¶
To get some practice with quantities, we’ve included an exercise where you can make and print some quantities, and then upgrade an existing function implementation from raw numbers to quantities.
Check out the Au 101: API Types Exercise!
Takeaways¶
-
We strive for unit safety. If we can check the unit-correctness of every individual line of code, by inspection, in isolation, we can reduce cognitive load, and write code faster and more robustly.
-
To store a raw numeric value safely inside of a quantity object, call the quantity maker whose name is the unit of interest.
- For example,
meters(3)
is quantity representing 3\,\text{m}, stored asint
.
- For example,
-
To retrieve a stored numeric value from a quantity
q
, callq.in(units)
, whereunits
was the quantity maker used in the first place.- For example,
meters(3).in(meters)
is simply3
.
- For example,
-
Quantity makers compose: you can multiply, divide, and raise them to powers to get a new quantity maker.
- For example,
(meters / second)
is a quantity maker which you can call like any other.
(meters / second)(5)
represents the quantity 5\,\text{m/s}.
- For example,
Tip
Au only contains unit-safe interfaces. That’s why simply storing the value in a quantity is enough to achieve unit-safety!
-
To take the example from
std::chrono::duration
, note that the system clock has different resolutions on different widely used toolchains. gcc uses 1\,\text{ns}, MSVC uses 100\,\text{ns}, and clang uses 1000\,\text{ns}. So if you subtracted two calls tostd::chrono::system_clock::now()
and called.count()
, your answers would vary by 3 orders of magnitude on different compilers! This is not to say that doing so would be a good use of the chrono library. It’s not, and that’s the point: a bare call to.count()
gives the reader no idea how to interpret its result. ↩ -
What about adding, subtracting, and comparing quantities of different units, but the same dimensions—like comparing
seconds(100)
tominutes(1)
, or addinginches(1)
tofeet(6)
? In most cases, we do support this as well, but it’s a more advanced usage which we’ll discuss further in future lessons. ↩