Measurement, Dimension, and Unit

Hey Everyone,

I'm opening a conversation on Measurement, Dimension, and Unit for the new Swift Foundation project that's recently been open-sourced. The goal is to get feedback from the community and eventually pitch a proposal.

I'm particularly interested in this topic since I created swift-measures and use it at work. It's a Swift Package with the aim to provide a more modern design with an extended list of units. It's open-sourced on GitHub and there's some documentation.

My Measure is very similar to Foundation's Measurement. It's Equatable, Hashable, Comparable, and Codable. It's initialised with a generic Unit, it's possible to do basic arithmetic, and it's convertible to other units of the same dimension.

However the units are slightly different than Foundation's. The key differences being they conform to Measureable, and they are all structures instead of classes. Creating new units is very convenient with MetricPrefix and BinaryPrefix.

There's room for improvement of course, the main changes I'm considering are:

  • Having a generic Value for Measure to provide more flexibility.
  • Removing the dependency on swift-numeric-protocols but keeping the same functionalities such as the ability to compare, add, subtract, multiply, and divide Measure.
  • Adding the ability to create ranges of measures.
  • Conforming Measure to CustomDebugStringConvertible, and CustomReflectable.
  • Building more derived units using base units as building blocks.
  • Implementing a format style to provide localized representations of measures.

Other considerations are:

  • Renaming Measure to Measurement.
  • Renaming all unit types suffixing the term Unit to follow Foundation's current naming convention.

I'm very much so looking forward to your feedback and ideas. I'm also looking for a co-author and/or mentor to help with the proposal.

12 Likes

Per another thread, @alexandrehsaad and I were comparing notes and I've done very similar things. A Measurement generic over Value (this was the impetus for my custom Measurement because I have written a RationalNumber Numeric that I wanted to use with it) and Unit. Structs instead of classes, etc... One other addition that I made was to have throwing versions of all arithmetic and conversions in case a caller (like myself) would prefer to get an Error instead of a crash on overflows. I've also done a bunch of work on FormatStyles for it and have been reviewing the newly released FormatStyle code to see how much of what I wrote fits Apple's usage (since the docs were so sparse and I was only able to make educated guesses about how things were written).

3 Likes

One interesting issue that I've come across is that you want to be able to pass in a FormatStyle for your Value, but since your Value is generic, that FormatStyle needs to be generic for the Measurement as well. That's one thing to work through.

The other is that you also want to be able to access the attributed version of that FormatStyle when you build the Measurement.FormatStyle.AttributedFormatStyle and currently that var attributed isn't part of any protocol, so I've had to add my own AttributableFormatStyle to expose it (easy since it does exist on the ints and floating points), but this should probably be a part of Foundation.

Hey @jasonbobier, thanks for sharing.

I'm not sure throwing versions of all arithmetic methods is in line with other Numeric types. I'm not against the idea though of adding it to the proposal and see from there.

I'd love to know more about your work/findings on FormatStyle.

The throwing versions really are meant as a replacement for the xReportingOverflow calls on Int (or in my case the RationalNumber which is using integers for its terms). Doubles handle overflows in their own way and you don't have to worry about them crashing if they are good going into the equation. Really, the call is just meant to be "do this arithmetic and but don't crash if it fails" which is necessary because the numbers can be very large and very small when dealing with some measurements.

1 Like

Another note is that Apple's Measurement work might be based upon ICU4C code, which could seriously limit the api without a rewrite.

Between these options:

static func + (a: T, b: T) -> T                // wraps on overflow
static func + (a: T, b: T) -> T                // traps on overflow
static func + (a: T, b: T) -> Optional<T>      // nil on overflow
static func + (a: T, b: T) -> Result<T, Error> // error on overflow
static func + (a: T, b: T) throws -> T         // throws on overflow
static func + (a: T, b: T) -> (T, overflow: Bool)

My personal choice would be a modification of the last option:

static func + (a: T, b: T) -> T // carries overflow information
var isOverflow: Bool

where overflow information is carried along, like in a floating math. Depending upon a calculation in question if you can tolerate designating one particular bit pattern of an integer value to mean "overflow" then no extra storage is required for an overflow bit: e.g. for signed short integers the logical choice of value is 0x8000 and for short unsigned integers - 0xFFFF).

The current design that I have doesn't have any extra storage for the Measurement itself. It is simply a Numeric and a Unit, so while the concrete type of the Numeric might contain overflow information, it isn't unified in any way.

I don’t think this is necessarily a good idea, if nothing else for thread safety reasons. I also don’t know that this would compose well with chained operations: if you do two additions in the same line of code, and the first one overflows but the second doesn’t, it’ll reset the variable to false, and you won’t know there was overflow.

1 Like

I meant isOverflow to be a property, not a global variable:

struct T {
    private var storage: Int16 // for example
    static func + (a: T, b: T) -> T // carries overflow information
    var isOverflow: Bool { ... }
}

Ha! My current version throws almost exactly those errors (overflow, undefined, incompatibleUnit errors).

Is the intent for Units to combine to form new Units? For example, multiplying two linear dimensions results in an area. I ran into this developing my own measurement code. I want to be able to express

2 m * 4 m == 6 m^2
6 m^2 * 2 m == 12 m^3

My Unit type, therefore, has a “dimension” quantity (and I still haven’t found a good name for that quantity).

This is fairly straightforward and easy enough to understand. But there are more complex combinations of units. Power (watt, W) is Energy (joule, J) multiplied by Time (second, s). More complex is something like Specific Impulse, which is Force per mass of fuel flow per second, which reduces to seconds (s).

An arbitrary set of operations across an arbitrary set of measurements can result in a complex derived unit that could be simplified, but in some cases it should be converted into a defined derived unit.

Moreover, there are types of measurement (length, time, amount, electric current, temperature, luminous intensity, and mass), from which all others are derived. Sometimes that derived unit is one of the fundamental units (specific impuls is “second”), sometimes it's a new unit (“watt”).

I have not yet developed a comprehensive representation that captures all of this.

1 Like

We really wanted to put operator overloading for measurements into the original design, but we received some feedback from the compiler team about the potential for a dramatic impact on type checking performance. There has been some further discussion here and here.

2 Likes

I think that's great idea. So we can't accidentally take sine of anything but a dimensionless quantity, add grams to meters, or even grams to kilograms without an explicit conversion.

I'd still give it a try and only backtrack if this potential actually happening in practice. If so - backtrack to either freestanding functions, or if that also slow as well – to instance methods:

// Plan A
func * (power: Power, time: Time) -> Energy { ... }
func * (force: Force, distance: Distance) -> Energy { ... }
func * (density: Density, volume: Volume) -> Mass { ... }
func * (charge: Charge, time: Time) -> Current { ... }
func * (velocity1: Velocity, velocity2: Velocity) -> VelocitySquared { ... }
func * (mass: Mass, velocitySquared: VelocitySquared) -> Energy { ... }

// Plan B
func mul(_ power: Power, _ time: Time) -> Energy { ... }
func mul(_ force: Force, _ distance: Distance) -> Energy { ... }
func mul(_ density: Density, _ volume: Volume) -> Mass { ... }
func mul(_ charge: Charge, _ time: Time) -> Current { ... }
func mul(_ velocity1: Velocity, _ velocity2: Velocity) -> VelocitySquared { ... }
func mul(_ mass: Mass, _ velocitySquared: VelocitySquared) -> Energy { ... }

// Plan C
extension Power { func mul(_ time: Time) -> Energy { ... }}
extension Force { func mul(_ distance: Distance) -> Energy { ... }}
extension Density { func mul(_ volume: Volume) -> Mass { ... }}
extension Charge { func mul(_ time: Time) -> Current { ... }}
extension Velocity { func mul(_ other: Velocity) -> VelocitySquared { ... }}
extension Mass { func mul(_ velocitySquared: VelocitySquared) -> Energy { ... }}
1 Like

Most units can be defined in terms of fundamental units (Force = Mass * Acceleration, but Acceleration = Distance / (Time * Time), so Force = Mass * Distance / Time * Time). Maybe unit conversions could happen by manipulating "numerator" and "denominator" generic types. That would let you have something like:

let mass: Unit<Mass, None> = .kilograms(100)
let speed: Unit<Distance, Time> = .metersPerSecond(10)
let momentum: Unit<(Mass, Distance), Time> = mass * speed
3 Likes

This?

let momentum: Unit<Mass, Unit<Time, Distance>> = mass * speed

Also, slightly non obvious but we can use Unit<A, Unit<Never, B>> to multiply A x B.

1 Like

That still doesn’t cover all situations: sometimes, units with non-integer powers are useful, such as V/sqrt(Hz).

2 Likes

You may certainly write that (including cases where power is irrational). However, can you give a practical example when that is useful instead of being a mathematical expression with no physical meaning?

That specific example I gave does have practical use, as a unit of noise. Its square is power/bandwidth, but it’s more useful to have voltage than power in the numerator, so you use voltage/sqrt(bandwidth).

1 Like

I wonder if a unit generic that’s specialized for the type of measurement will make it hard to reduce complex unit types.