Measurements and Units with Phantom Types

Datetime:2016-08-23 04:46:32          Topic: IOS Development           Share

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:

struct Measurement<UnitType: Unit> {
    let unit: UnitType
    var value: Double
}

let length = Measurement(value: 5, unit: UnitLength.meters)
// length is a Measurement<UnitLength>

Measurement got the value-type treatment — it is a class in Objective-C and a struct in Swift.

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:

struct MyMeasurement<UnitType: MyUnit> {
    var value: Double

    init(_ value: Double) {
        self.value = value
    }
}

let length = 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:

/// Represents a physical quantity or “family of units”.
/// Examples: length, temperature, velocity.
protocol UnitFamily {
    associatedtype BaseUnit
}

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

/// Represents a unit of measurement.
/// Examples: meters, kilometers, miles, seconds, hours, degrees Celsius.
protocol MyUnit {
    associatedtype Family: UnitFamily

    static var symbol: String { get }
    static var converter: UnitConverter { get }
}

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.

// MARK: - Length
enum Length: UnitFamily {
    typealias BaseUnit = Meters
}

enum Meters: MyUnit {
    typealias Family = Length
    static let symbol = "m"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 1)
}

enum Kilometers: MyUnit {
    typealias Family = Length
    static let symbol = "km"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 1000)
}

// MARK: - Duration
enum Duration: UnitFamily {
    typealias BaseUnit = Seconds
}

enum Seconds: MyUnit {
    typealias Family = Duration
    static let symbol = "s"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 1)
}

enum Minutes: MyUnit {
    typealias Family = Duration
    static let symbol = "min"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 60)
}

enum Hours: MyUnit {
    typealias Family = Duration
    static let symbol = "hr"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 3600)
}

// MARK: - Speed
enum Speed: UnitFamily {
    typealias BaseUnit = MetersPerSecond
}

enum MetersPerSecond: MyUnit {
    typealias Family = Speed
    static let symbol = "m/s"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 1)
}

enum KilometersPerHour: MyUnit {
    typealias Family = Speed
    static let symbol = "km/h"
    static let converter: UnitConverter = UnitConverterLinear(coefficient: 1.0/3.6)
}

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 .

extension MyMeasurement {
    /// Converts `self` to a measurement that has another unit of the same family.
    func converted<TargetUnit>(to target: TargetUnit.Type) -> MyMeasurement<TargetUnit>
        where TargetUnit: MyUnit, TargetUnit.Family == UnitType.Family
    {
        let valueInBaseUnit = UnitType.converter.baseUnitValue(fromValue: value)
        let valueInTargetUnit = TargetUnit.converter.value(fromBaseUnitValue: valueInBaseUnit)
        return MyMeasurement<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:

extension MyMeasurement: CustomStringConvertible {
    var description: String {
        return "\(value) \(UnitType.symbol)"
    }
}

extension MyMeasurement: ExpressibleByIntegerLiteral {
    init(integerLiteral value: IntegerLiteralType) {
        self.value = Double(value)
    }
}

extension MyMeasurement: ExpressibleByFloatLiteral {
    init(floatLiteral value: FloatLiteralType) {
        self.value = value
    }
}

In use

Now we can create measurements and convert them to other units. The expressible-by-literal syntax is quite nice:

let fiveMeters: MyMeasurement<Meters> = 5
// → 5.0 m
let threeKilometers: MyMeasurement<Kilometers> = 3
// → 3.0 km
threeKilometers.converted(to: Meters.self)
// → 3000.0 m
threeKilometers.converted(to: Seconds.self)
// error: 'Family' (aka 'Length') is not convertible to 'Family' (aka 'Duration') (as expected)

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:

func delay(after duration: 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):

func delay<Time>(after duration: MyMeasurement<Time>, block: () -> ())
    where Time: MyUnit, Time.Family == Duration
{
    // ...
}

It works, but it makes the function signature significantly less readable, even with the where clause out of the way .

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:

func + <Unit1, Unit2> (lhs: MyMeasurement<Unit1>, rhs: MyMeasurement<Unit2>) -> MyMeasurement<Unit1>
    where Unit1: MyUnit, Unit2: MyUnit, Unit1.Family == Unit2.Family
{
    let rhsConverted = rhs.converted(to: Unit1.self)
    return MyMeasurement(lhs.value + rhsConverted.value)
}

fiveMeters + threeKilometers
// → 3005.0 m
threeKilometers + fiveMeters
// → 3.005 km

Again, note the constraint Unit1.Family == Unit2.Family to prevent adding seconds to meters .

Multiplication with a scalar value is even easier because no unit conversions are involved. We simply multiply the value and create a new measurement. Two overloads are needed for a * b and b * a :

func * <UnitType> (measurement: MyMeasurement<UnitType>, scalar: Double) -> MyMeasurement<UnitType> {
    var result = measurement
    result.value *= scalar
    return result
}

func * <UnitType> (scalar: Double, measurement: MyMeasurement<UnitType>) -> MyMeasurement<UnitType> {
    return measurement * scalar
}

threeKilometers * 2
// → 6.0 km
let twoSeconds: MyMeasurement<Seconds> = 2
60 * twoSeconds
// → 120.0 s

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:

/// Describes this relation between units:
/// Product = Factor1 * Factor2
protocol Product: MyUnit {
    associatedtype Factor1: MyUnit
    associatedtype Factor2: MyUnit
}

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.

func * <Unit1, Unit2, Result> (lhs: MyMeasurement<Unit1>, rhs: MyMeasurement<Unit2>) -> MyMeasurement<Result>
    where Result: Product, Result.Factor1.Family == Unit1.Family, Result.Factor2.Family == Unit2.Family
{
    let left = lhs.converted(to: Result.Factor1.self)
    let right = rhs.converted(to: Result.Factor2.self)
    return MyMeasurement(left.value * right.value)
}

a = c × b

func * <Unit1, Unit2, Result> (lhs: MyMeasurement<Unit2>, rhs: MyMeasurement<Unit1>) -> MyMeasurement<Result>
    where Result: Product, Result.Factor1.Family == Unit1.Family, Result.Factor2.Family == Unit2.Family
{
    return rhs * lhs
}

This is exactly the same as the previous function, with lhs and rhs reversed. The implementation simply forwards to the other overload.

b = a / c and c = a / b

func / <Unit1, Unit2, Result> (lhs: MyMeasurement<Result>, rhs: MyMeasurement<Unit2>) -> MyMeasurement<Unit1>
    where Result: Product, Result.Factor1.Family == Unit1.Family, Result.Factor2.Family == Unit2.Family
{
    let right = rhs.converted(to: Result.Factor2.self)
    return MyMeasurement(lhs.value / right.value)
}

func / <Unit1, Unit2, Result> (lhs: MyMeasurement<Result>, rhs: MyMeasurement<Unit1>) -> MyMeasurement<Unit2>
    where Result: Product, Result.Factor1.Family == Unit1.Family, Result.Factor2.Family == Unit2.Family
{
    let right = rhs.converted(to: Result.Factor1.self)
    return MyMeasurement(lhs.value / right.value)
}

Again, these follow the same ideas, only the placement of the generic parameters varies.

Concrete implementation

Now it is finally possible to express the relation length = speed × duration (i.e. speed = length / duration ):

extension Meters: Product {
    typealias Factor1 = MetersPerSecond
    typealias Factor2 = Seconds
}

And it can be used like this:

let tenMeters: MyMeasurement<Meters> = 10
let fourSeconds: MyMeasurement<Seconds> = 4
let speed: MyMeasurement<MetersPerSecond> = tenMeters / fourSeconds
// → 2.5 m/s

let thirtyKilometersPerHour: MyMeasurement<KilometersPerHour> = 30
let twoHours: MyMeasurement<Hours> = 2
let tripLength: MyMeasurement<Meters> = thirtyKilometersPerHour * twoHours
// → 60000.0 m
tripLength.converted(to: Kilometers.self)
// → 60.0 km

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.





About List