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.
Other dimensionless units and 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”.