I was at Jesse Squires’s talk at iOS Conf Singapore, where he discussed Swift’s numeric types and protocols. That reminded me of SE-0104, Protocol-oriented integers and now that the implementation is in Swift 4, thought it would be a good time to have a closer look.
Let’s start with the protocol hierarchy for Int
, everyone’s favorite integer type:
As with most types in the standard library, Int
conforms to a whole lot of protocols! All the way down the chain, Numeric
is one of the base protocols. All the different sized integer types (UInt
, Int8
, etc.) as well as floating-point types (Float
, Double
, etc.) conform to Numeric
.
Numeric, Starting From 0x00
You can follow along with the standard library source in Integers.swift.gyb, although as usual, all the relevant code will be inline below.
Here’s the protocol declaration to start:
public protocol Numeric : Equatable, ExpressibleByIntegerLiteral {
Two additional conformances here:
Equatable
adds support for the==
operator.ExpressibleByIntegerLiteral
allows for code likelet a: Int = 42
where42
is the aforementioned “integer literal”.
That means all numeric types can be initialized with integer literals but not float literals. This bit of code is OK:
let someFloat: Float = 42 // ✅
But this is not OK:
let someInt: Int = 4.2 // 🙅
You’ll run into the ExpressibleByFloatLiteral
protocol in specific floating-point types, but not at the level of general Numeric
types.
Initialization
The protocol has one required initializer:
init?<T : BinaryInteger>(exactly source: T)
It’s a failable initializer, and the “exactly” in the argument label should tell you why: if you try to initialize an instance with a value beyond what it can hold, the initializer will return nil
:
let ok = Int8(exactly: 10) // 10
let tooBig = Int8(exactly: 300) // nil
Also note the constraint on the source type: it has to conform to BinaryInteger
. BinaryInteger
conforms to Numeric
so it seems a little weird for a “parent” protocol like Numeric
to need a type that conforms to a “child” protocol like BinaryInteger
to construct itself. What’s going on here?
Representation vs Use
A quick sidebar on exactly what these two protocols BinaryInteger
and Numeric
are for.
According to the header docs, BinaryInteger
is just what it says on the tin: “An integer type with a binary representation”. That’s pretty familiar to many programmers as we use integers measured in bits all the time.
Numeric
, on the other hand, isn’t so much about representation as it is about usage. The protocol “provides a suitable basis for arithmetic on scalar values”.
OK, back to arithmetic then. 🤓
Magnitude
Magnitude is the absolute value of the number, so 42.magnitude
and -42.magnitude
are both 42
.
associatedtype Magnitude : Comparable, Numeric
var magnitude: Magnitude { get }
The magnitude
computed property has to be Numeric
and also Comparable
. That means a number itself doesn’t have to be comparable, but its magnitude does.
When you look at the concrete implementation of types such as Int
, you’ll see it uses magnitude
to do some arithmetic, calculate distance between numbers, etc.
Arithmetic
We’ve reached our favorite arithmetic operation: addition!
static func + (_ lhs: Self, _ rhs: Self) -> Self
static func +=(_ lhs: inout Self, rhs: Self)
The first addition function takes two values and produces the sum; the second is the mutating version where the “left hand side” argument (the a
in a += 10
) is mutated.
In your own numeric types, you should at a minimum provide the implementation for the mutating operators.
If you’ve coded types that conform to Equatable
, you’ll remember you need to provide an implementation for ==
and then you get !=
for free.
Similarly, the usual pattern is to define +
in terms of +=
. For example, the implementaton of +
in the concrete standard library type UInt16
uses +=
:
// Inside the UInt16 implementation
public static func +(_ lhs: UInt16, _ rhs: UInt16) -> UInt16 {
var lhs = lhs
lhs += rhs
return lhs
}
Finally, the Numeric
protocol also requires operators for subtraction and multiplication:
static func - (_ lhs: Self, _ rhs: Self) -> Self
static func -=(_ lhs: inout Self, rhs: Self)
static func * (_ lhs: Self, _ rhs: Self) -> Self
static func *=(_ lhs: inout Self, rhs: Self)
Same guidelines apply, where you should provide the mutating version for your own conforming types.
Things Left Unsaid
Concrete numeric types do much more than what’s defined here in this one protocol. As you saw from the diagram at the top of this post, there are a lot of protocols put together to make something as complex as a Swift integer.
That said, we’ve only covered Numeric
and seen just a slice of all the functionality we’re used to from a numeric type. What are some of the big things missing?
Division — we’ve seen addition, subtraction, and multiplication, but what of the missing arithmetic family member? Division is defined separately in the
BinaryInteger
andFloatingPoint
protocols.The function definitions in the protocols are the same, but the specs are slightly different: integer division discards the remainder, and floating-point division follows the IEEE-754 rules.
Floating-point things — you saw how types that conform to
Numeric
are alsoExpressibleByIntegerLiteral
but not expressible by float literals. Integers can be converted to floats in a straightforward manner, but you need to consider rounding when going from float to integer. How should this rounding work? That’s something out of scope forNumeric
.Comparable — again, this is something found on the
BinaryInteger
andFloatingPoint
protocol level, except for themagnitude
property you saw earlier. Both those protocols conform toStrideable
, which in turn impliesComparable
.
The Closing Brace
The Numeric
protocol provides the fundamentals of numeric types:
- Initialization with integer values
- Equatable
- Simple arithmetic
- The concept of a magnitude to determine its underlying value
In addition to the missing things listed in the previous section, the protocol also has no opinion on storage. Remember, Numeric
is about what numbers are used for, while the other protocols down the line such as BinaryInteger
are about representation and how the values are stored.
Ready to build your own custom vigesimal numeric type yet? 😉
}