Measurements and Units with Phantom Types

This is a bonus post in my little series on the new units of measurement types in Foundation. While I like Apple’s API, I thought it could be interesting to explore a slightly different approach to the same problem. Particularly, I was interested in the question if a pure Swift design could be significantly better than Apple’s interface, for which Objective-C compatibility inevitably is a prime consideration
.

Apple’s design

The primary data type for users of Apple’s API is Measurement
. It contains a floating-point value
and the unit
the value is measured in. It is generic over the unit type:

structMeasurement<UnitType:Unit>{letunit:UnitTypevarvalue:Double}letlength=Measurement(value:5,unit:UnitLength.meters)// length is a Measurement<UnitLength>

Families of units, such as length
or duration
, are modeled as types in a class hierarchy: Unit
> Dimension
> UnitLength
, UnitDuration
, etc. Specific units, such as meters
or kilograms
, are instances of their unit family class. Each unit is composed of the unit’s symbol
( "kg"
) and a unit converter
object that encodes the instructions how to convert the unit to the family’s base unit
.

Phantom types

What if we modeled specific units also as types
instead of instances
? If we had types named Meters
, Kilometers
, or Miles
, we could design a generic measurement type that had just a single stored property for the quantity. The quantity’s unit would be entirely encoded in the type itself:

structMyMeasurement<UnitType:MyUnit>{varvalue:Doubleinit(_value:Double){self.value=value}}letlength=MyMeasurement<Meters>(5)// length is a MyMeasurement<Meters>

Again, note the difference between the two approaches: in Apple’s API, Measurement
is parameterized with the unit family length
; the specific unit meters
is part of the value. In my design, the generic parameter is the specific unit meters
.

MyMeasurement
is also called a phantom type
because the generic parameter UnitType
does not appear anywhere in the type’s definition. Its only purpose is to differentiate two types like MyMeasurement<Meters>
and MyMeasurement<Kilometers>
from each other so that they cannot be substituted.

We’ll see later whether this is actually useful in our situation because you could argue that a measurement in meters should
be totally interchangeable with a measurement in kilometers. For other examples of phantom types in Swift, see this objc.io article
or this talk by Johannes Weiß
. The Swift standard library also makes use of phantom types, for example with UnsafePointer<Memory>
.

Benefits

One obvious benefit of my approach is the 50% smaller size of the measurement data type because the reference to the unit
instance is not needed. (Unit instances themselves are shared between all measurements in that unit: two measurements of 5 meters
and 10 meters
reference the same unit instance.) The size savings are offset by a potentially much larger code size because the compiler has to generate more specializations of the generic type and functions using the type.

Since Unit
is a reference type in Apple’s API, passing Measurement
values to functions also incurs some retain/release overhead. Both of these factors are unlikely to be significant in a typical app, and I haven’t investigated them further. They have certainly not been important for me while exploring these ideas.

Concrete design

We still need to specify how to define units in this system. Units are grouped into unit families
, such as length, temperature, or duration. Let’s start by defining a protocol for a unit family:

Just as in Apple’s API, each unit family must define a base unit
, which is used to convert between units of the same family. For example, the base unit for the family length
should be meters
. We model this as an associated type of the UnitFamily
protocol. This has the advantage that the base unit is encoded in the type system. In Foundation, base units must be documented separately to allow others to extend the system with custom units.

The next piece is the MyUnit
protocol for modeling specific units, which in Apple’s design would be instances of a unit family type. (I’m using the My
prefix to avoid naming conflicts with Apple’s types.)

A unit declares the family it belongs to through an associated type. It also defines static properties for its symbol (such as "m"
for meters or “lbs” for pounds) and a unit converter that describes how to convert the unit to the family’s base unit. For example, if the base unit for the family Length
is Meters
, the converter for Kilometers
should be UnitConverterLinear(coefficient: 1000)
. The unit converter for the base unit itself should always have the coefficient 1
. I’m borrowing the UnitConverter
class from Foundation.

Foundation makes a distinction between Unit
for dimensionless units and Dimension
for dimensional units. We don’t do this here for simplicity; all our units are dimensional.

A base unit must be a unit, of course, so ideally the associated type BaseUnit
in UnitFamily
should have a corresponding constraint BaseUnit: MyUnit
. Unfortunately, that creates a circular reference between the two protocols, and that is currently not permitted in Swift. Everything works fine without the constraint, though.

Conforming to the protocols

It’s time to add some concrete implementations for these protocols. I’m showing length, duration, and speed here, with a few units each. It would be trivial to add more units (such as miles
or centimeters
) or entirely different unit families (such as temperature
).

I chose to use enums over structs for the types because caseless enums have the nice property that they cannot be instantiated. This is perfect for us because we are only interested in the types, not in instances of the types.

Converting measurements

Now that we can represent measurements in different units, we need a way to convert between them. The converted(to:)
method takes the type of a target unit and returns a new measurement in that unit, using the unit converters. Note the constraint TargetUnit.Family == UnitType.Family
, which limits conversions to the same unit family. The compiler will not let you convert Meters
to Seconds
.

extensionMyMeasurement{/// Converts `self` to a measurement that has another unit of the same family.funcconverted<TargetUnit>(totarget:TargetUnit.Type)->MyMeasurement<TargetUnit>whereTargetUnit:MyUnit,TargetUnit.Family==UnitType.Family{letvalueInBaseUnit=UnitType.converter.baseUnitValue(fromValue:value)letvalueInTargetUnit=TargetUnit.converter.value(fromBaseUnitValue:valueInBaseUnit)returnMyMeasurement<TargetUnit>(valueInTargetUnit)}

Let’s also add some convenience functionality to MyMeasurement
. Adding conformance to CustomStringConvertible
provides us with a nice debugging output, and conforming to ExpressibleByIntegerLiteral
and ExpressibleByFloatLiteral
makes creating new measurements from literals much more pleasant:

What about the use of measurements as function arguments? Take this hypothetical delay
function that takes a duration and a closure and executes the closure after the specified duration:

funcdelay(afterduration:MyMeasurement<Seconds>,block:()->()){// ...}

In this form, the function requires a measurement in seconds. If you want to call it with an argument in milliseconds, it is your responsibility to convert the value. It has the advantage of ensuring type safety over a simple TimeInterval
argument — the compiler will not allow you to pass a MyMeasurement<Milliseconds>
—, but it is significantly less flexible than the equivalent Measurement<UnitDuration>
, which would accept any duration unit.

We can achieve this, too, by making the function generic over the unit type (with the constraint that it must have a Duration
family):

For this reason alone, Apple’s design where units are instances not types is probably more practical. And arguably it also makes more sense. After all, meters
and kilometers
are just different notations for essentially the same thing. But this is an exploration that doesn’t have to make sense, so let’s continue.

Addition and scalar multiplication

It should be possible to add two measurements of the same family together, even if they have different units. This is quite easy to implement with a generic overload of the +
operator. As a convention, we convert the right-hand side value to the left-hand side’s unit and return the result in terms of the that unit:

Multiplication and division (i.e. physics)

If you rememberpart 2 of this series, my original goal was to model how unit families are related to each other, e.g. speed = length / duration
or energy = power × time
. To do this, I introduced a protocol named UnitProduct
, and unit families could express the factors they are composed of by conforming to the protocol and naming their factors as associated types.

Let’s do the same here, but now we are going to express relationships directly between units
, not unit families
. The Product
protocol looks very similar:

Note that a single protocol is sufficient to describe both multiplicative and fractional relations because a = b × c
is equivalent to b = a / c
. The choice is arbitrary, and whatever you choose makes expressing some relations feel less natural. For example, if we want to express speed = length / duration
, we have to rewrite it first as length = speed × duration
.

The next step is to implement the actual computations, i.e. overloads for the multiplication and division operators that work on types conforming to our protocol. We need four variants:

a = b × c

The generic constraints make this quite complicated. For any type Result
that conforms to Product
, this overload defines the multiplication of any two measurements whose units Unit1
and Unit2
have the same families as the units specified in Result.Factor1
and Result.Factor2
. The result is computed by converting the measurements to Result.Factor1
and Result.Factor2
, respectively, and multiplying those.

It works quite well, but two drawbacks are apparent. First, the compiler cannot infer the return types of the computations automatically at the moment. I don’t know if improvements to the compiler can solve this in the future or if I could provide more help by specifying better generic constraints to the functions. I experimented with this a little bit, but could not make it work.

Second, while the arguments’ units need only have the correct family, the unit of the return type is currently limited to the specific unit used in the Product
protocol. So something like let tripLength: MyMeasurement<Kilometers> = ...
would not work, you have to take the result in Meters
first and then convert it. I consider this a pretty big limitation.

Conclusion

Regardless of the (very real) flaws of this design, note that not a single line of executable code is needed to add this mathematical relation to the type system! Simply by adding the protocol conformance (which only involves defining two associated types), we literally added the proposition 1 meter = 1 m/s × 1 s
to the compiler’s pool of “truth”. And if you wanted to add another relation (such as 1 J = 1 W × 1 s
), adding another protocol conformance is all that’s required.

I find this fascinating.

Nonetheless, I do not think this API based on phantom types is superior to the one in Foundation. It simply makes more sense to parameterize measurements based on unit families rather than units.