Skip to content

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”.