Making a Numeric Type in Swift
Programmers invariably work with numbers that represent values with intrinsic types, or units. Types can be conceptual representations of real world modeling such as distance, mass, time. Those conceptual types can be formalized into units, such as meters, grams, seconds.
Naturally we want to be able to manipulate these numbers with types. I want to take the distance from my apartment to my favorite coffee shop, and the distance from the coffee shop to my office, add them together and know the distance of my commute. I may want to add the time for each of those trips to find the total time. But if I were to add a distance to a time, the result is meaningless. We are going to explore how Swift can help us avoid committing such an error.
tl;dr here’s the gist. But the journey really is worthwhile. Please read on.
If you’ve worked with Cocoa, you may have encountered types like NSTimeInterval
and CLLocationDistance
. Hopefully you’ve favored using them over naked numeric types for your properties. Not only do they allow for easier interoperation with system frameworks, they also provide a conventional basis for understanding what your values represent.
// according to convention, you know this is in seconds
@property (nonatomic, assign) NSTimeInterval elapsedTime;
// hours? minutes? jiffy?
@property (nonatomic, assign) double mysteryTime;
// according to convention, you know this is in meters@property (nonatomic, assign) CLLocationDistance totalDistance;// meters? fathoms? potrzebies?@property (nonatomic, assign) double secretDistance;
Unfortunately, there’s nothing preventing you from committing type atrocities:
unknowableWhatever = elapsedTime + totalDistance;
Swift can rescue us with types! Right? Unfortunately, it is perfectly valid to arbitrarily combine those various types in a Swift program.
var elapsedTime :NSTimeInterval = 12
var mysteryTime :Double = 24
var totalDistance :CLLocationDistance = 917
var secretDistance :Double = 1.726
let sadness = elapsedTime + totalDistance
But wait! I thought Swift was supposed to prevent this sort of thing! That’s what types are all about!
In Swift, types such as NSTimeInterval
and CLLocationDistance
are “type aliases”. That is, they say they are one thing but it is a new name for what they actually are. They are a Double
in everything but name. Swift is designed to closely interoperate with C, and Apple’s frameworks (and most any other non-trivial C code) makes extensive use of typedef
. typedef
is critically important in writing portable C code, as well as enabling language features like structs as types. Further, some of those types are truly intended to be mixed, such as CLLocationDistance
and CLLocationAccuracy
. And while we are exploring an avenue for a unit-safe numeric type, we will see later that strictly enforced boundaries between arithmetic types can be sometimes undesirable.
Swift’s typealias
seems to be, in part, a concession to C’s typedef
, though it is also used in the construction of complex type relationships with generics.
That said, it is entirely possible for the work contained in the entire rest of this article to be obviated by a simple new Swift language feature to strengthen the power of typealias
// This is not real Swift, nor do I have any reason to believe it ever will be.
// But I could imagine this meaning that Distance is to behave exactly like a Double,
// yet also remain a strictly different type.
strict typealias Distance = Double
Structs for Type Safety
Structs, whether in C or Swift, can provide us with the type safety we need. We can define a Distance
struct like so
/// Type used to represent distance in meters
struct Distance {
var value :Double
}
Now we can’t arbitrarily add distances and time intervals. But we can’t add distances to distances either. We can create functions to do that (again, this works in C as well with appropriate syntactical changes)
func add(a: Distance, b: Distance) -> Distance {
return Distance(value: a.value + b.value)
}
But where Swift really begins to shine is in overloading operators with our own functions. We can define +
as a function that operates on a pair of Distance
, and proceed to to freely add distances as though they were numbers. We could implement function after function until we have all the operators we might need, ==,<=,>=,!=,+,-,*,/,++,--
and so on. But we can save ourselves some work and integrate very closely with how Swift is built by taking a principled approach to our new numeric type.
More Principled with Protocols
Many of Swift’s fundamental language features are expressible in Swift itself. Swift’s collection protocols allow us to write first-class collections, a process that has been outlined in NSHipster. There are protocols that can help us on the way to creating first-class numbers.
Swift protocols are much like Objective-C protocols. They represent a sort of contract – a class or struct can adopt the protocol if and only if it fulfills all the requirements of the protocol. The protocol’s requirements look a lot like a struct or a class except they lack implementation of functions. By adopting protocols and fulfilling their requirements, we can leverage functionality built right into Swift that sits on top of those protocols.
Comparisons
Comparisons start with the Equatable
protocol. Equatable
only defines one required function, ==
, and the documentation states
When adopting `Equatable`, only the `==` operator is required to be implemented. The standard library provides an implementation for `!=`.
We can grow our definition of Distance as follows with ==
implemented and !=
provided by the standard library. Note that if we implement ==
but do not conform to Equatable
, !=
will not be available.
struct Distance: Equatable {
var value :Double
}
func ==(lhs: Distance, rhs: Distance) -> Bool {return lhs.value == rhs.value}
Next up the protocol hierarchy is Comparable
, which itself conforms to Equatable
.
A type conforming to `Comparable` need only supply the `<` and `==` operators; default implementations of `<=`, `>`, `>=`, and `!=` are supplied by the standard library::
We can implement <
, drop Equatable
in favor of Comparable
and end up with
struct Distance: Comparable {
var value :Double
}
func ==(lhs: Distance, rhs: Distance) -> Bool {return lhs.value == rhs.value}
func <(lhs: Distance, rhs: Distance) -> Bool {return lhs.value < rhs.value}
Literal Convertibles
A literal, in Swift and other languages, is a value that appears to be baked in to the source code. In the following code, the characters “1” and “2” together (and “1”, “2”, “.” and “0”) are a numeric literal representing the number 12.
let w = 12
let x :Double = 12
let y :Int = 12
let z = 12.0
When compiled, type constraints are applied to determine what type literals should be. If they are not of a specific type, they end up as IntegerLiteralType
or FloatLiteralType
, which in turn are typealias
es for Int
and Double
Swift exposes the mechanisms underlying literals to developers. With Literal Convertibles, we can create constructs that feel first-class, much in the same way as collection protocols and operator overloading. NSHipster has a nice overview of the available literal convertibles and some of their uses. Here, we will examine the IntegerLiteralConvertible
and FloatLiteralConvertible
types which were not covered in their article but are critical to implementing our own numeric type.
With what we’ve built so far, to create a Distance
, we must use an initializer:
let twelve = Distance(value: 12)
let eleven = Distance(value: 11)
let ten :Distance = 10 // This will not compile because '10'
// isn't a Distance, it's a literal
What we would like to do is the following:
let twelve :Distance = 12
let eleven :Distance = 11
To avoid overloading the default initializer in Comparable
, we will implement IntegerLiteralConvertible
in an extension as follows, and thereby gain the ability to assign integer literals directly into Distance
typed containers.
extension Distance :IntegerLiteralConvertible {
init(integerLiteral value: IntegerLiteralType) {
self.init(value: Double(value))
}
}
With integer literal convertibles, we still cannot assign floating point literals (e.g. 1.75
) directly. That has its own protocol that is much the same as its integer counterpart. In our present case considering distances represented as a Double
under the hood, we will implement FloatLiteralConvertible
. It is not so difficult to imagine purely integral types for which the protocol should not be implemented.
extension Distance :FloatLiteralConvertible {
init(floatLiteral value: FloatLiteralType) {
self.init(value: Double(value))
}
}
Unfortunately at this point, we begin to run out of runway with literal convertible types.
let one :Distance = 1 // Ok
let two :Distance = 2.0 // Ok
let six = two + 4 // ‘Int’ is not convertible to ‘Distance’let five :Distance = 3 + 2 // ‘Int’ is not convertible to ‘Distance’let four = 2.0 + two // ‘Double’ is not convertible to ‘Distance’
Arithmetic
We can start enabling arithmetic with the SignedNumberType
protocol. SignedNumberType
conforms to IntegerLiteralConvertible
, so we can replace our integer literal extension’s protocol and implement two new required functions: negation and subtraction
// Replace `Distance :IntegerLiteralConvertible` from before with a new extension signature
extension Distance :SignedNumberType {
init(integerLiteral value: IntegerLiteralType) {
self.init(value: Double(value))
}
}
// Subtractionfunc -(lhs: Distance, rhs: Distance) -> Distance {return Distance(value:lhs.value - rhs.value)}
// Negation (notice the prefix
keyword)prefix func -(x: Distance) -> Distance {return Distance(value:-x.value)}
We now gain the ability to create Distance
s from arithmetically combined integer literals – as long as they’re combined with subtraction. We also get abs
for free at this point even though we don’t implement AbsoluteValuable
let four :Distance = 5 - 1 // Ok
let five :Distance = 7 - two // Ok
let negativeSix :Distance = -6
let six = abs(negativeSix) // 6
To Integer or Not to Integer?
We now have a choice before us. There are four remaining basic operators to implement. +,*,/,%
. For Double
and other floating point types, Swift seems to declare these operations in a free-standing form with no associated protocol. However integers all conform to IntegerType
, which includes an IntegerArithmeticType
. The fundamental arithmetic operations are supported by a small family of functions, which if implemented would give us those operators:
static func addWithOverflow(lhs: Self, _ rhs: Self) -> (Self, overflow: Bool)
static func subtractWithOverflow(lhs: Self, _ rhs: Self) -> (Self, overflow: Bool)
static func multiplyWithOverflow(lhs: Self, _ rhs: Self) -> (Self, overflow: Bool)
static func divideWithOverflow(lhs: Self, _ rhs: Self) -> (Self, overflow: Bool)
static func remainderWithOverflow(lhs: Self, _ rhs: Self) -> (Self, overflow: Bool)
But for IntegerType
, things expand quickly aside from IntegerArithmeticType
. There are seemingly unrelated protocols such as RandomAccessIndexType
, which includes BidirectionalIndexType
and _RandomAccessIndexType
, which in turn pull in many, many protocols enabling the type to be used for things like indexing, ranges, incrementing. All this amounts to 11 separate protocols (or 19 if you count the _
-prefixed protocols) that you must implement to fulfill the requirements of being an integer in Swift. This is not counting whether you decide to go down the SignedIntegerType
or UnsignedIntegerType
paths.
We are not implementing an integer. So we will not conform the the IntegerType
in any form. There is no floating point corresponding protocol for arithmetic, so we will just implement a couple of arithmetic functions.
func +(lhs: Distance, rhs: Distance) -> Distance {
return Distance(value:lhs.value + rhs.value)
}
func %(lhs: Distance, rhs: Distance) -> Distance {return Distance(value:lhs.value % rhs.value)}
And now we can do basically what we want.
let one :Distance = 1
let two :Distance = 2.0
let three = two + 1
let negativeSeven :Distance = -7let seven = abs(negativeSeven)
let six = three + threelet five = six - 1let four = 5 - one
let eight = 4 + fourlet nine = 10 - one
Multiplication
Be warned: if you are planning on creating your own numeric type for any kind of real-world application: the type of Distance * Distance
is not Distance
but Distance
2 or Area
. The combination of types as units is not something that the Swift type system can likely dynamically account for. For certain units, Area
being a likely candidate, it may be worthwhile to implement a more elaborate system to account for common use cases:
func *(lhs: Distance, rhs: Distance) -> Area {
return Area(value:lhs.value * rhs.value)
}
func /(lhs: Area, rhs: Distance) -> Distance {return Distance(value:lhs.value / rhs.value)}
This approach will only take you so far. So be warned that due to the nature of multiplication, implementing *
and /
on a type that needs to behave like a unit is likely a bad idea. It would be better to implement your domain-specific elaborate operations directly on the underlying Double
within a carefully constructed type-safe function.
Additionally, when constructing a numeric type to account for units, if an integer representation seems like a good way to go, consider that conforming to the various integer protocols forces you to implement potentially dangerous multiplication and division functions.
So multiplication and division will be left out of our Distance
unit for now.
Bookkeeping
Among the protocols we skipped over when deciding to not implement an IntegerType are Printable
and Hashable
. These are good protocols to conform to whenever possible. Printable
lets the object play nicely with string interpolation, can help with debugging, and makes the new type behave more like a built-in number. Hashable
is another protocol implemented by the native numeric types. Implementing a hash value is a good idea for creating value types. It can act as a shortcut to determining the uniqueness of values without doing a direct comparison. It also enables the types use in hash-map based data structures such as Set
and Dictionary
.
extension Distance :Printable {
var description: String { get {
return value.description
}
}
}
extension Distance :Hashable {var hashValue: Int { get {return ~Int(value._toBitPattern())}}}
Done!
Putting it all together we now have a Distance
unit with some lovely arithmetic properties. But what if we want to make another unit, say, a TimeInterval
? This is a lot of boilerplate to copy.
struct Distance: Comparable {
var value :Double
}
extension Distance :FloatLiteralConvertible {init(floatLiteral value: FloatLiteralType) {self.init(value: Double(value))}}
extension Distance :SignedNumberType {init(integerLiteral value: IntegerLiteralType) {self.init(value: Double(value))}}
extension Distance :Printable {var description: String { get {return value.description}}}
extension Distance :Hashable {var hashValue: Int { get {return ~Int(value._toBitPattern())}}}
func ==(lhs: Distance, rhs: Distance) -> Bool {return lhs.value == rhs.value}
func <(lhs: Distance, rhs: Distance) -> Bool {return lhs.value < rhs.value}
func -(lhs: Distance, rhs: Distance) -> Distance {return Distance(value:lhs.value - rhs.value)}
prefix func -(x: Distance) -> Distance {return Distance(value:-x.value)}
func +(lhs: Distance, rhs: Distance) -> Distance {return Distance(value:lhs.value + rhs.value)}
func %(lhs: Distance, rhs: Distance) -> Distance {return Distance(value:lhs.value % rhs.value)}
Don't Repeat Yourself: Generics
This is programming. Once we get it working, we’re just getting started. Time to refactor!
All of the arithmetic functions we provided an implementation for are dependent on two things: the Distance
type, and the fact that the value
property within that type contains a type that already has the desired operations defined on it. With Swift’s generics, we can take advantage of this arrangement and create a protocol to reduce the amount of work we have to do to make a new type.
With that in mind, we can create a new NumericType
protocol that looks like just the parts of Distance
that we want to abstract. We want a value
property, and we want all the protocols we explored above.
public protocol NumericType : Comparable, FloatLiteralConvertible, IntegerLiteralConvertible, SignedNumberType {
var value :Double { set get }
init(_ value: Double)
}
Now we can write functions that operate on instances of that protocol. We may be tempted to write our first function to like so
public func + (lhs: NumericType, rhs: NumericType) -> NumericType {
return NumericType(lhs.value + rhs.value)
}
This is going to present a few problems. The first of which is that we can’t make an instance of NumericType
. It’s a protocol, and we can’t instantiate a protocol. While this isn’t an insurmountable problem, it’s not quite worth fixing because we have a more serious problem:
The point of creating NumericType
is so we can create instances such as Distance
and TimeInterval
. They will each conform to NumericType
and therefore be perfectly valid arguments to +
. So we would end up right back where we started:
public struct Distance :NumericType {
...
}
public struct TimeInterval :NumericType {…}
public func + (lhs: NumericType, rhs: NumericType) -> NumericType {return NumericType(lhs.value + rhs.value) // This won’t even compile}
let time :TimeInterval = 5let Distance :Distance = 12let untypedChaos = time + Distance
Generics provide a way to write flexible and reusable code that can operate on a variety of types. But most importantly, and much more powerfully than protocols, generics are a way to constrain code to operate on types that match certain conditions. This is easiest to see by example.
public func + <T :NumericType> (lhs: T, rhs: T) -> T {
return T(lhs.value + rhs.value)
}
What this does is, between the < >
, say that there is a type T
that must be a NumericType
. Then we go on to say that +
accepts a pair of T
, and returns a T
. In the implementation we create a new T
, which we can do because we’ve said we can in the protocol. Whenever this function is called, every T
must correspond to just one type. So
public struct Distance :NumericType {
...
}
public struct TimeInterval :NumericType {…}
public func + (lhs: T, rhs: T) -> T {return T(lhs.value + rhs.value)}
let time :TimeInterval = 5let Distance :Distance = 12let untypedChaos = time + Distance // This won’t compile. Chaos averted!
Let’s follow this pattern to create a new fully-fleshed NumericType
we can reuse.
public protocol NumericType : Comparable, FloatLiteralConvertible, IntegerLiteralConvertible, SignedNumberType {
var value :Double { set get }
init(_ value: Double)
}
public func % (lhs: T, rhs: T) -> T {return T(lhs.value % rhs.value)}
public func + (lhs: T, rhs: T) -> T {return T(lhs.value + rhs.value)}
public func - (lhs: T, rhs: T) -> T {return T(lhs.value - rhs.value)}
public func < (lhs: T, rhs: T) -> Bool {return lhs.value < rhs.value}
public func == (lhs: T, rhs: T) -> Bool {return lhs.value == rhs.value}
public prefix func - (number: T) -> T {return T(-number.value)}
public func += (inout lhs: T, rhs: T) {lhs.value = lhs.value + rhs.value}
public func -= (inout lhs: T, rhs: T) {lhs.value = lhs.value - rhs.value}
And we can now trim down our specific implementation of types and keep repeated down to more reasonable levels. Repeat this for any numeric types you want to make sure don’t mix with incompatible numbers.
public struct Distance :NumericType {
public var value :Double
public init(_ value: Double) {
self.value = value
}
}
extension Distance :IntegerLiteralConvertible {public init(integerLiteral: IntegerLiteralType) {self.init(Double(integerLiteral))}}
extension Distance :FloatLiteralConvertible {public init(floatLiteral: FloatLiteralType) {self.init(Double(floatLiteral))}}
It is through the protocols we adopted and generics on the implementations that the Swift standard library is able to provide !=
, >
and all the other functions we didn’t have to write for types that didn’t exist at the time of writing. Not only have we used these language features, we also followed the same pattern to create a powerful new protocol that save us and others time in the future.
By defining types (and protocols) around capabilities, and writing functions that target those capabilities, we end up with cleaner and remarkably reusable code when compared to implementing functionality targeting a specific type. The bulk of our final implementation has nothing to do with distances, time intervals, apples, oranges, or anything else. It reads more like a set of statements of fact that any future programmer could adopt if they so chose.
These kinds of patterns have always been possible. The only difference is that Swift very nicely lets us apply them in a way that using the resulting structures feels much more like writing native Swift.