Algebraic types in the Swift type system

While studying Software Engineering (in college), I recall hearing that Math is part of every programming language. Still, I never understood the Math abstractions applied to them (the programming languages). However, this truly changed some years ago when I started learning functional programming and changing my mindset (functional thinking).

There are different math branches - such as Category Theory, Set Theory, Algebra, and so on - that can help us understand math abstractions found in a type system. Each one will drive us to a different understanding. This time I'll only focus on Algebra.

In my own words - I am not a mathematician - some of the info that Algebra can give is the different states that the system may have, and by this, I mean all the different values that the type can have/hold.

Algebra - Products and Sums

In school, when speaking about the Product and Sum, we always look at them as Multiplication (Product) and Addition (Sum). To understand how these concepts are applied to the Swift type system, let's go through them from a practical perspective.

Before digging deeper, I consider it relevant to have in mind the following:

  • Product is a combination AB, meaning A and B together.
  • Sum (a.k.a. Coproduct) is an alternation A|B, meaning A or B but not both.

Products

Tuples and Structs in Swift have many similarities, and I may say, we can consider both Products.

For example, we can define a pretty similar API with both types.

// using a tuple
typealias Product<A, B> = (a: A, b: B)

// using a struct
struct Product<A, B> {
    let a: A
    let b: B
}

In both cases, either using a tuple or a struct, we can create a product value that holds the exact same information.

let product = Product(a: false, b: false)

Why do we consider this a Product? - Well, like I said above...

Product is a combination AB, meaning A and B together.

The "together" word in the sentence is important. Let's elaborate on a more concrete example.

We have a Priority enum that describes 3 possible states: low, medium, and high.

enum Priority {
    case low
    case medium
    case high
}

And we also have a Card struct that holds the information about the Card's visibility and priority.

struct Card {
    let isVisible: Bool
    let priority: Priority
}

The possible states for the information we can hold in the Card product are 6.

Card(isVisible: false, priority: .low)    // 1
Card(isVisible: false, priority: .medium) // 2
Card(isVisible: false, priority: .high)   // 3
Card(isVisible: true, priority: .low)     // 4
Card(isVisible: true, priority: .medium)  // 5
Card(isVisible: true, priority: .high)    // 6

A different way to understand this is that the possible states for the Card are "Bool times Priority" - Wait... What?...

Yes...

  • Bool has 2 possible values: false and true.
  • Priority has 3 possible values: low, medium, and high.

So, we can say that "Bool * Priority = 6" (2 * 3 = 6).

Here is where we can see the Product (Multiplication) algebra of the Card type.

Sums

On the other hand, contrary to tuples and structs, we can consider Enums as Sums.

In Swift, enumerations have finite (countable) values - that is why they can be "enumerated".

As mentioned above...

Sum (a.k.a. Coproduct) is an alternation A|B, meaning A or B but not both.

Let's go through the next example...

A common situation in Scrum for a developer on a sprintly-basis is to define the weight of the stories.

Imagine that we support 2 different ways to size stories: using Fibonacci and T-Shirt sizes.

// Fibonacci.swift

enum Fibonacci: Int {
    case one = 1
    case two = 2
    case three = 3
    case five = 5
    case eight = 8
    case thirteen = 13
}

// TShirtSize.swift

enum TShirtSize {
    case extraSmall
    case small
    case medium
    case large
    case extraLarge
}

And we also have an Either type (as enum) that will help us to define a StoryWeight type.

enum Either<A, B> {
    case left(A)
    case right(B)
}

typealias StoryWeight = Either<Fibonacci, TShirtSize>

We are using Either to provide a clearer idea of the A|B alternation.

So, the different possible states for the StoryWeight (a.k.a. Either<Fibonacci, TShirtSize>) are 11.

StoryWeight.left(.one)          // 1
StoryWeight.left(.two)          // 2
StoryWeight.left(.three)        // 3
StoryWeight.left(.five)         // 4
StoryWeight.left(.eight)        // 5
StoryWeight.left(.thirteen)     // 6
StoryWeight.right(.extraSmall)  // 7
StoryWeight.right(.small)       // 8
StoryWeight.right(.medium)      // 9
StoryWeight.right(.large)       // 10
StoryWeight.right(.extraLarge)  // 11

Then, we can say that possible value representations for StoryWeight are "Fibonacci plus TShirtSize = 11" (6 + 5 = 11).

Misused Algebra in Swift

We might be asking ourselves: When can we find it helpful to understand the difference between Product and Sum types?... Well... I think - this is my opinion and might be wrong - that Swift has some APIs that we could take as good examples of misused algebra.

For example, the URLSession API.

func dataTask(
    with url: URL,
    completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionDataTask

The closure part of the function is "returning" a tuple (Product) of 3 Optional types:

(Data?, URLResponse?, Error?)

And based on what we have been talking about (reading) in this article, this API tells that we may get: "Data? times URLResponse? times Error?" possible responses. I presume this must be due to the interoperability with Obj-C - who knows?!.

Anyways... If we read the documentation, it says...

If the request completes successfully, the data parameter of the completion handler block contains the resource data, and the error parameter is nil. If the request fails, the data parameter is nil and the error parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, the response parameter contains that information.

According to the docs, when success, we get the Data and the Error is nil, and vice-versa.

So, I would say that a better way to describe this would be using the Result (Sum) type instead of the tuple (Product).

func dataTask(
    with url: URL,
    completionHandler: @escaping (Result<(Data, URLResponse), Error>) -> Void
) -> URLSessionDataTask

By doing this, the "complexity" of the code decreases, and the handling of the "success" and "failure" becomes easier.

Swift kind of improved this in the new Concurrency API. For example, when defining the async/await version of the same function, they did this:

func data(
    from url: URL,
    delegate: URLSessionTaskDelegate? = nil
) async throws -> (Data, URLResponse)

With this function signature, they tell us that if there is an Error, it is thrown. If not, we will get the Data and the URLResponse. It is pretty much the same thing we described with the use of the Result type.

Technically, we could write a new function (for Concurrency API) that gets rid of the throws and returns a Result instead.

func data(
    from url: URL,
    delegate: URLSessionTaskDelegate? = nil
) async -> Result<(Data, URLResponse), Error>

TL;DR

  • The Algebras applied to the Swift type system are Product and Sum.
  • From an Algebraic perspective, we can clearly understand the possible states that the system can represent by understanding the composition between Products and Sums.
  • Product is a combination AB, meaning A and B together.
    • We can consider Tuples and Structs as Products.
  • Sum (a.k.a. Coproduct) is an alternation A|B, meaning A or B but not both.
    • We can consider Enums as Sums.
  • When finding a misused algebra in the Swift API, we can extend it (writing extensions) and "improve" it by applying the correct algebra.
    • URLSession is an excellent example of a misused algebra implemented in the API.