• Swift
  • Foundation

Type-safe Measurements in Swift

Discover how to manage units with the Foundation framework's Measurement struct, enabling type-safe management of units.

Written by Jacob Gelman

Handling units of measurement accurately is crucial in software development, where errors in unit handling can lead to serious logical errors. Swift’s Foundation framework provides a powerful solution with its generic Measurement struct—allowing for the management of numeric quantities with associated units in a type-safe way. This article delves into how using the Measurement struct can streamline working with different units of measure, leading to code that is more concise and free of logical errors.

Let's begin with a simple example of calculating perimeter from width and height measurements. At first width and height are both specified in meters, and then height is changed to be expressed in feet instead of meters:

Both in metersMeters & feet
let width = Measurement(value: 2, unit: UnitLength.meters)
let height = Measurement(value: 3, unit: UnitLength.meters)

let perimeter = (2 * width) + (2 * height) // 10m

Both in metersMeters & feet
let width = Measurement(value: 2, unit: UnitLength.meters)
let height = Measurement(value: 3, unit: UnitLength.meters)

let perimeter = (2 * width) + (2 * height) // 10m

Regardless of whether height is expressed in feet or meters, we effectively get the same result. Since both measurements represent lengths, arithmetic operations can be correctly performed between them regardless of the exact unit of length used to express each one; arithmetic operations are performed on a measurement's underlying value only after being automatically converted to the base unit (meters in the case of length).

Measurement is a generic struct whose definition looks like this:

struct Measurement<UnitType> where UnitType : Unit { ... }
struct Measurement<UnitType> where UnitType : Unit { ... }

The generic, UnitType, keeps track of what type of quantity (e.g. length, mass, etc.) a measurement represents and bakes that information directly into its type. Consequently, two measurements representing different types of quantities are not the same type, and thus trying to perform arithmetic operations between incompatible measurements will produce a compile time error:

let capacity = Measurement(value: 256, unit: UnitInformationStorage.gigabytes)
let length = Measurement(value: 10, unit: UnitLength.meters)

let someQuantity = capacity + length
let capacity = Measurement(value: 256, unit: UnitInformationStorage.gigabytes)
let length = Measurement(value: 10, unit: UnitLength.meters)

let someQuantity = capacity + length
Error: Cannot convert value of type 'Measurement<UnitLength>' to expected argument type 'Measurement<UnitInformationStorage>'

This error is raised because the + operator is not overloaded for operands of type Measurement<UnitLength> and Measurement<UnitInformationStorage>. Since combining these units logically does not make sense, this error effectively highlights the presence of a logical error in the code. However, there are situations where it makes sense to perform arithmetic between two measurements with different unit types—such as in the case of derived units. Consider calculating speed, a unit rate, as the quotient of distance and time:

let distance = Measurement(value: 50, unit: UnitLength.kilometers)
let time = Measurement(value: 1, unit: UnitDuration.hours)

let speed = distance / time
let distance = Measurement(value: 50, unit: UnitLength.kilometers)
let time = Measurement(value: 1, unit: UnitDuration.hours)

let speed = distance / time
Binary operator '/' cannot be applied to operands of type 'Measurement<UnitLength>' and 'Measurement<UnitDuration>'

Though this isn't supported out of the box, it is trivial to define a custom extension leveraging the type system and operator overloading to make this work. Specifically, we can overload the binary / operator on Measurement<UnitSpeed> to accept a length measurement as its left operand and a duration measurement as its right operand. In the implementation, we convert the two measurements to meters and seconds respectively, take their quotient, and use it to construct a speed measurement expressed in meters per second:

extension Measurement<UnitSpeed> {

/// Form speed, a unit rate, as the quotient of distance and time.
public static func /(
lhs: Measurement<UnitLength>,
rhs: Measurement<UnitDuration>
) -> Self {
Measurement(
value: lhs.converted(to: .meters).value /
rhs.converted(to: .seconds).value,
unit: .metersPerSecond
)
}
}
extension Measurement<UnitSpeed> {

/// Form speed, a unit rate, as the quotient of distance and time.
public static func /(
lhs: Measurement<UnitLength>,
rhs: Measurement<UnitDuration>
) -> Self {
Measurement(
value: lhs.converted(to: .meters).value /
rhs.converted(to: .seconds).value,
unit: .metersPerSecond
)
}
}

With the extension in place, dividing distance by time just works:

let distance = Measurement(value: 50, unit: UnitLength.kilometers)
let time = Measurement(value: 1, unit: UnitDuration.hours)

let speed = distance / time // ~13.8m/s
// Measurement<UnitSpeed> ✔️
let distance = Measurement(value: 50, unit: UnitLength.kilometers)
let time = Measurement(value: 1, unit: UnitDuration.hours)

let speed = distance / time // ~13.8m/s
// Measurement<UnitSpeed> ✔️

Though this is a simple example, imagine an application performing long, multi-step calculations involving various measurements and units. This type of application would particularly benefit from using the Foundation framework's measurement APIs to prevent logical errors that arise from performing arithmetic operations between incompatible units. By using the Measurement struct and defining custom extensions as needed to enable inter-unit operations, developers can create code that is not only more concise, since unit conversions are performed automatically, but also statically guaranteed to be correct in terms of unit agreement.