Dimensionless Units and Quantities¶
Every dimension has a variety of units available to measure its quantities. This is no less true for the “null” dimension! Example units include “dozen”, “score”, “percent”, and others. We call these units (and their quantities) “dimensionless”.
One big difference (compared to units of other dimensions) is that the magnitudes of dimensionless
units are objectively meaningful. Recall that for, say, length-dimensioned units, there is no
such thing as “the” magnitude of Feet
. We can choose any number we like, as long as it’s 12 times
the magnitude of Inches
. By contrast, Percent
has a definite magnitude: it’s 1/100.
Unos
: the “unit 1”¶
Dimensionless units are special, but one is more special still. Literally, one — the dimensionless unit whose magnitude is 1. It is the only unit equal to its own square, and the only unit whose quantities are completely and unambiguously interchangeable with raw numbers.
In our library, we named this unit “unos”, after an SI proposal from the 1990s. Although
the proposal failed, the concept turns out to suit software libraries much better than scientific
prose. It is short, greppable, and reasonably intuitive. It also lets us enter and exit the library
boundaries in just the same way as for other units: q = unos(x)
turns a numeric value x
into
a Quantity q
, and q.in(unos)
retrieves the raw number.
This is particularly useful when working with non-unos
dimensionless units. For example: say we
wanted to “express 0.75
as a quantity of percent
”. Instead of trying to remember whether to
multiply or divide by 100, we can simply write x = unos(0.75).as(percent)
. And if we have
something that’s already a percent
, but we want its “true” value, we can simply write
x.in(unos)
.
Exact cancellation and types¶
Sometimes a computation exactly cancels all units (like the ratio of two lengths, each measured in
Feet
). As a units library, we have two options: return a Quantity of Unos
, or a raw number.
Presently, we opt for the latter; here is why.
Users generally tend to expect the result of a perfectly unit-cancelling expression to behave
exactly like a raw number, in every respect. Although a Quantity<Unos, T>
implicitly converts
to T
, this conversion turns out to get triggered in only a subset of use cases; many edge cases
remain. The only way to perfectly mimic a raw number is to return one.
The downside is that this incurs some complexity. This mainly impacts generic code, where we
can’t know whether a product or quotient of Quantities is a Quantity, or a raw number. People
writing generic code are generally more advanced users, and thus better able to work around this
inconsistency. For example, one could write an ensure_quantity(T x)
function template, which
returns unos(x)
in the generic case, but has an overload for when T
matches Quantity<U, R>
that simply returns x
.
We may someday be able to improve the ergonomics of Quantity<Unos, T>
to the point that we’d feel
comfortable returning it, thus making the library more consistent. However, returning a raw number
feels like the right compromise solution for us to start with.
Note
For results that are dimensionless but not “unitless”, we always return a Quantity.
For example, milli(seconds)(50) * hertz(10)
produces a numeric value of 50 * 10 -> 500
, in
a dimensionless unit whose magnitude is 1 / 1000. This is equivalent to a raw numeric value
of 1 / 2 — but it’s not the library’s place to decide how or when to perform the lossy
conversion of this integral Quantity. Rather, the library’s job is to safely hold the obtained
numeric value of 500
. The Magnitude attached to the Quantity is what lets us do so.
Raw number conversions¶
Converting dimensionless quantities to raw numbers raises several interesting and subtle questions. For dimensionless quantities that are also unitless, it’s straightforward: implicit conversion is always safe, and we always permit it. However, dimensionless quantities with non-trivial magnitudes require more careful consideration.
Implicit conversions¶
A common choice among units libraries is to support implicit conversions with dimensionless units.
This is intuitively appealing: after all, a Quantity like percent(75.0)
represents the value
0.75
. Shouldn’t we handle that conversion automatically, just as happily as we turn feet(3)
into inches(36)
?
While the appeal is obvious, we believe this does more harm than good. The reason is that
a Quantity has two different notions of value, and for dimensionless units specifically, these
become ambiguous. Consider something like inches(24)
. By “value”, we might mean:
- the numeric variable
24
, stored safely within the Quantity object, as if in a container. - the quantity value itself — in this case, the extent of the physical length, which is identical
with
feet(2)
.
With dimensioned quantities, the library prevents confusion: we can’t use either in contexts where
the other belongs. But dimensionless quantities lack this safeguard. This opens the door to
decisions which are individually reasonable, but which interact badly together. For instance,
a Quantity<Percent, T>
may be implicitly constructible and convertible with T
, but could pick
up stray factors of 100 in the round trip!
It is safer (and not much less convenient!) to use separate, unambiguous idioms for these two notions of “value”.
Explicit conversions¶
We have seen why implicit conversions are problematic, but it’s still important to support
explicit conversions robustly. For this, we have the
as_raw_number()
utility. The name disambiguates
between the two notions of “value”, making it clear that we are considering the quantity
holistically (as with any other as
-named interface). So, something like
as_raw_number(percent(75.0))
could only ever mean 0.75
.
Additionally, as_raw_number
gets all the same safety checks as any other conversion function,
guarding against both truncation and overflow. For example, as_raw_number(percent(150))
would
fail to compile, because the true value of 1.5 cannot be represented in int
. We can even use the
same mechanisms to turn off conversion risks: so,
as_raw_number(percent(150), ignore(TRUNCATION_RISK))
would compile, and produce 1
.
Summary¶
Any time your computation produces a dimensionless result, consider keeping it as a Quantity
type.
You get the benefit of having the compiler keep track of any scale factors, such as the
\frac{1}{100} associated with Percent
. When you do want just a raw number, though, pass the
result to as_raw_number
. You’ll make your intent clear, get all the usual safety checks, and even
get the ability to selectively override safety checks when you know that’s the right move for your
use case.