[swift unboxed]

Safely unboxing the Swift language & standard library


Swift's Numeric Protocol

What’s in a Swift number?

6 November 2017 ∙ Protocols ∙ written by

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:

Protocol hierarchy for Int

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 like let a: Int = 42 where 42 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.

a 16-bit binary number

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

arithmetic operators plus, minus, mulitply, divide

Everyone’s favorite arithmetic operations.

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 and FloatingPoint 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 also ExpressibleByIntegerLiteral 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 for Numeric.

  • Comparable — again, this is something found on the BinaryInteger and FloatingPoint protocol level, except for the magnitude property you saw earlier. Both those protocols conform to Strideable, which in turn implies Comparable.

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? 😉

}