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
, meaningA
andB
together. - Sum (a.k.a. Coproduct) is an alternation
A|B
, meaningA
orB
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
, meaningA
andB
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
andtrue
.Priority
has 3 possible values:low
,medium
, andhigh
.
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
, meaningA
orB
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 theA|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 and returns a throws
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
, meaningA
andB
together.- We can consider Tuples and Structs as Products.
- Sum (a.k.a. Coproduct) is an alternation
A|B
, meaningA
orB
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.