Vector Space Representations¶
To understand why vector space representations are so important for units libraries, we’ll dig into the most fundamental example. After that, we’ll look at some other instances of vector space representations in our library.
Consider Dimensions. How can we teach the library to recognize
that, say, the product of the Dimensions Speed
and Time
is the Dimension Length
? If these
three Dimensions are all primitive, irreducible objects, this is very challenging. However, if
Speed
is just an alias for (Length / Time)
, then it’s easy to see that (Length / Time)
* Time
reduces to Length
.
This is what we do: we single out certain Dimensions and call them “Base Dimensions”. Any valid choice must fulfill these conditions:
-
Independence: no product of rational powers of Base Dimensions is dimensionless, unless all exponents are 0.
-
Completeness: every Dimension of interest can be represented as some product of rational powers of Base Dimensions.
The reader may recognize these properties of Base Dimensions as analogous to the defining properties of Basis Vectors in a vector space, but with these differences:
- instead of adding vectors, we multiply Base Dimensions
- instead of multiplying vectors by a scalar, we raise them to a power
In fact, we can bridge this gap if we consider the exponents of the dimensions to be the scalars of the vector space. In a sense, the “logarithms” of the dimensions form a vector space; in this case, over the rationals.
This is the “vector space representation” for Dimensions.
Fleshing out the analogy¶
How do the defining properties of vector spaces manifest themselves here? Let the space of all Dimensions be \mathscr{D}. For any dimensions D, D_1, D_2, D_3 \in \mathscr{D}, and any rational numbers a, b \in \mathbb{Q}, we have:
- Associativity: D_1 \cdot (D_2 \cdot D_3) = (D_1 \cdot D_2) \cdot D_3
- Commutativity: D_1 \cdot D_2 = D_2 \cdot D_1
- Identity (Vector): \exists \pmb1 \in \mathscr{D}: \,\, \pmb1 \cdot D = D \cdot \pmb1 = D, \,\, \forall D
- Inverse: \forall D \in \mathscr{D}, \exists D^{-1} \in \mathscr{D}: \,\, D \cdot D^{-1} = D^{-1} \cdot D = 1
- Scalar/Field Multiplication Compatibility: (D^a)^b = D^{(ab)}
- (Recall that “scalar multiplication” in “ordinary” vector spaces corresponds to exponentiation in our vector space.)
- Identity (Scalar): \exists 1 \in \mathbb{Q}: \,\, D^1 = D
- Distributivity (Vectors): (D_1 \cdot D_2)^a = D_1^a \cdot D_2^a
- Distributivity (Scalars): D^{(a + b)} = D^a \cdot D^b
C++ Implementation Strategies¶
The abstract concepts above form the core of basically every C++ units library. When it comes to implementation, there are a variety of choices.
Naive approach: positional arguments¶
The simplest implementation of a vector space is to use positional template parameters to represent the coefficients (exponents) of each basis vector (base dimension). For example:
// Basic approach (not used in this library)
template<typename LengthExp, typename TimeExp>
struct Dimension;
using Length = Dimension<std::ratio<1>, std::ratio<0>>;
using Time = Dimension<std::ratio<0>, std::ratio<1>>;
using Speed = DimQuotientT<Length, Time>;
This approach is easy to implement, but its simplicity comes at a cost.
-
Compiler errors are inscrutable. (What exactly does
Dimension<std::ratio<1, 1>, std::ratio<-1, 1>>
represent?) -
If we need to add a new basis vector, it will affect an immense number of callsites.
-
Some applications need infinitely many basis vectors! This approach is a complete non-starter.
Advanced approach: variadic templates¶
We can solve all of these problems by making Dimension
a variadic template.
// Advanced approach (the one we use, although simplified here)
template<typename... BaseDimPowers>
struct Dimension;
using Length = Dimension<base_dim::Length>;
using Time = Dimension<base_dim::Time>;
using Speed = DimQuotientT<Length, Time>; // As before.
Compiler errors are now easy (or at least possible) to read: when we see something like
Dimension<base_dim::Length, Pow<base_dim::Time, -1>>
, we can recognize it as “Speed”. And adding
new basis vectors—even arbitrarily many new ones—doesn’t affect any existing callsites.
The downside is that the added complexity incurs new risk. Now we have to care about the order
of the template parameters; otherwise, we could have different types representing the same
conceptual Dimension. Fortunately, that’s exactly why we built the //au:packs
target: to handle these subtleties robustly.
Other vector space representations¶
Above, we focused on Dimensions as an example use case for the vector space representation. Though by far the most common in units libraries, it’s not the only one that adds value. Here are some others worth recognizing.
Magnitude¶
The ratio between two Units of the same Dimension is a positive real number: a “Magnitude”. We use a vector space representation for Magnitudes, because then it will naturally support all the same operations which Dimensions support. But then, what are the basis vectors? What numbers can we use that are “independent”, in the sense that every Magnitude gets a unique representation?
Prime numbers are a great start! Given any collection of primes, \{p_1, \ldots, p_N\}, and corresponding rational exponents \{a_1, \ldots, a_N\}, the product p_1^{a_1} \cdot (\ldots) \cdot p_N^{a_N} is unique: no other collection \{a_1, \ldots, a_N\} can produce the same number1.
This already lets us represent anything we could get with std::ratio
. And, unlike a
(num, denom)
representation, we’re always automatically in lowest terms: any common factors cancel
out automatically when we represent it via its prime factorization!
In fact, we have surpassed std::ratio
’s functionality, too. We can handle very large numbers
with negligible risk of overflow: yotta
(10^{24}) doesn’t even fit in std::intmax_t
, but
pow<24>(mag<10>())
2 handles it with ease. We can even handle radicals: something unthinkable for
std::ratio
, like \sqrt{2}, is as easy as root<2>(mag<2>())
3.
Finally, we can incorporate other irrational numbers, too. No units library is complete without
robust support for \pi, but std::ratio
isn’t up to the task. For vector space magnitude
representations, though, its difficulty becomes a strength. We know there is no collection of
exponents \{a_i\} such that \pi = \prod\limits_{i=1}^N p_i^{a_i}, for any collection of primes
\{p_i\}. This means that \pi is independent, and we can add it as a new basis vector. Then
the ratio of, say, Degrees
to Radians
(i.e., \pi / 180) could be expressed as
PI / mag<180>()
4.
Units¶
If we form a Unit by combining other units—say, Miles{} / Hours{}
—it’s useful to retain the
identities of the units that went into it. There are several reasons to prefer this to, say,
converting everything to a coherent combination of preferred “base units”, and some Magnitude for
scaling.
-
It will be easy to generate a compound label, by combining the primitive labels for
Miles
andHours
. -
Compiler errors will mention only familiar, recognizable Units.
-
It promotes cancellation where appropriate:
Miles{} / Hours{}
timesHours{}
will give simplyMiles{}
.
Our treatment of Units differs from other vector space instances, because we prefer not to use the
container type (in this case, UnitProduct<...>
) unless we have to: after all, Meters
is more
user-friendly than UnitProduct<Meters>
, let alone something awful like
UnitProduct<RatioPow<Meters, 1, 0>>
! We support this use case with the following strategy:
-
wrap-if-necessary on the way in (via
AsPackT
) -
unwrap-if-possible on the way out (via
UnpackIfSoloT
)
This was the only machinery we needed to add: apart from that, we were able to leverage our pre-existing packs support to provide a fluent experience for compound units.
-
Technically, this is only true for a finite collection of primes, though the collection can be arbitrarily large. If we took an infinite collection of primes, it wouldn’t just give some numbers multiple representations — it would give every number uncountably infinitely many distinct representations! In practice, this distinction is largely academic, because our library currently only targets computers with finite amounts of memory. ↩
-
pow<24>(mag<10>())
expands toMagnitude<Pow<Prime<2>, 24>, Pow<Prime<5>, 24>>
. ↩ -
root<2>(mag<2>())
expands toMagnitude<RatioPow<Prime<2>, 1, 2>>
. ↩ -
PI / mag<180>()
expands toMagnitude<Pow<Prime<2>, -2>, Pow<Prime<3>, -2>, Pi, Pow<Prime<5>, -1>>
. ↩