Swift Enums Are 'Sum' Types. That Makes Them Very Interesting

intermediate

swifttype-theoryenumsios

Algebraic Types—What Are They?

Algebraic types… seems like the kind of math-y word Haskell programmers would use. But in reality, you don’t need to know type theory or have a PhD in mathematics to understand them. Algebraic types aren’t new types. They’re simply a new way of thinking about the types you already know.

There are many different algebraic types—in fact, all of the types you currently use are algebraic as well. Here, we’ll cover two basic algebraic types:

  • Product types
  • Sum types

So let’s start with the familiar stuff.

Product Types

Product types are nothing more than Swift struct types, or Java class types. They’re called product types because the number of possible values they can have is the product of the number of possible values of their constituent parts.

struct ProductExample {
    let a: Bool
    let b: Bool
}

A Bool type can have 2 possible values. ProductExample has 2 Bool types. We can get the number of possible values of ProductExample by multiplying the number of possible values of Bool a with the number of possible values of Bool b. So the number of possible values of ProductExample is 2×2=42 \times 2 = 4.

This is evident with all the possible instances of the type:

let first = ProductExample(a: true, b: true)
let second = ProductExample(a: true, b: false)
let third = ProductExample(a: false, b: true)
let fourth = ProductExample(a: false, b: false)

Let’s look at another example:

struct ProductExampleTwo {
    let a: Bool
    let b: Int8
}

Now our type ProductExampleTwo has the number of possible values which is a multiple of Bool and Int8. Int8 has 256 possible values, Bool has 2 possible values. So our ProductExampleTwo has 512 possible values.

In general, without going into type theory notation, we can define a function NpvN_{pv} which for a given type returns the number of possible values that type has. So:

Npv(Bool)=2N_{pv}(Bool) = 2 Npv(Int8)=256N_{pv}(Int8) = 256 Npv(String)=N_{pv}(String) = \infty

The conclusion that Npv(String)=N_{pv}(String) = \infty has certain implications in the way we think about types, but those can be omitted for this article without compromising the general message.

If we use the NpvN_{pv} function, we can express a general case for all product types.

Let’s assume there exists type TT which has constituent parts T1,T2,T3,,TnT_1, T_2, T_3, \ldots, T_n. TT can be considered a product type if:

Npv(T)=Npv(T1)×Npv(T2)×Npv(T3)××Npv(Tn)N_{pv}(T) = N_{pv}(T_1) \times N_{pv}(T_2) \times N_{pv}(T_3) \times \ldots \times N_{pv}(T_n)

Or in more proper notation:

Npv(T)=i=1nNpv(Ti)N_{pv}(T) = \prod_{i=1}^{n} N_{pv}(T_i)

Sum Types

If you’re unfamiliar with enum syntax, there’s a quick and dirty intro at the bottom of the article.

So if structs are product types, then what are sum types? Simple—sum types (in Swift) are enums!

The number of possible values of a sum type is the sum of the number of possible values of its constituent parts.

Now that we know how to deal with the algebraic view of types and have our NpvN_{pv} function handy, let’s explore the world of sum types with examples.

enum SumExample {
    case a(Bool)
    case b(Bool)
}

Let’s see all the different ways we can instantiate the SumExample enum:

let first = SumExample.a(true)
let second = SumExample.b(true)
let third = SumExample.a(false)
let fourth = SumExample.b(false)

There are 4 ways to instantiate the SumExample enum. This number comes from the fact that Npv(Bool)=2N_{pv}(Bool) = 2 and that SumExample contains two Bool types, and 2+2=42 + 2 = 4.

Let’s examine a different example:

enum SumExampleTwo {
    case a(Bool)
    case b(Int8)
}

Now what is the number of possible values of SumExampleTwo? It’s the sum of possible values of Bool and Int8. So:

Npv(Bool)=2N_{pv}(Bool) = 2 Npv(Int8)=256N_{pv}(Int8) = 256 Npv(SumExampleTwo)=Npv(Bool)+Npv(Int8)N_{pv}(SumExampleTwo) = N_{pv}(Bool) + N_{pv}(Int8) Npv(SumExampleTwo)=2+256=258N_{pv}(SumExampleTwo) = 2 + 256 = 258

Expressing a General Case

Let’s assume there exists a type TT with constituent parts T1,T2,T3,,TnT_1, T_2, T_3, \ldots, T_n. TT can be considered a sum type if:

Npv(T)=Npv(T1)+Npv(T2)+Npv(T3)++Npv(Tn)N_{pv}(T) = N_{pv}(T_1) + N_{pv}(T_2) + N_{pv}(T_3) + \ldots + N_{pv}(T_n)

Or in more proper notation:

Npv(T)=i=1nNpv(Ti)N_{pv}(T) = \sum_{i=1}^{n} N_{pv}(T_i)

How Can I Use This to Write Better Code?

All this theoretical stuff is fine and dandy, but let’s see some practical examples.

In general, you want to follow the mantra:

The number of possible values of your type should be equal to the number of possible values of your use case.

1. Result Enum

So what does this mean?

Let’s assume you’re making a REST call and getting back some String as a result. A bad way to write this would be:

typealias Handler = (String?, Error?) -> Void

func getUser(from: URL, completionHandler: Handler) {
    // function implementation
}

getUser(from: someUrl) { result, error in
    if let result = result {
        // Handle result
    }
    if let error = error {
        // Handle error
    }
}

Why is this a bad option? Because our use case has two possible values:

  1. Success — result was fetched from the server
  2. Error — something went wrong during the process

And our implementation has four possible values:

result = nil, error = not nil     // Case 1
result = not nil, error = nil     // Case 2
result = not nil, error = not nil // Case 3
result = nil, error = nil         // Case 4

We have to think about the semantics of this approach. We’re actually counting on only two cases: Case 1 and Case 2. What does it mean when both result and error are nil? Or when they’re both not nil?

The problem is we’re using a product type where we should be using a sum type. The solution:

enum Result {
    case success(String)
    case error(Error)
}

typealias Handler = (Result) -> Void

func getUser(from: URL, completionHandler: Handler) {
    // implementation
}

getUser(from: someUrl) { response in
    switch response {
    case .success(let result):
        print(result)
    case .error(let error):
        print(error.localizedDescription)
    }
}

We’ve created a sum type called Result and we’re using it to distinguish between two possibilities. Our use case matches our implementation and all is good.

2. Optional Enum

You might not have known that Swift has a built-in sum type that we use almost all of the time: Optional.

The optionals we know and love (or sometimes hate) are implemented inside of the Swift language as an enum:

enum Optional<T> {
    case some(T)
    case none
}

So let a: String? = "Hello" would just be shorthand syntax for let a = Optional.some("Hello").

The good thing is that Swift has some neat syntax sugar to help us distinguish sum types—the if let and guard let constructs.

This:

let a: String? = "Hello"

if let a = a {
    print(a)
} else {
    print("error")
}

is equivalent to this:

let a = Optional.some("Hello")

switch a {
case .some(let res):
    print(res)
case .none:
    print("Error")
}

3. Router and Theme Patterns

Some things in your apps have a finite number of possibilities and are ultra easy to express as sum types—like an API endpoint router:

enum Router {
    case user(id: Int)
    case weather(day: Day)
}

extension Router {
    var url: String {
        switch self {
        case .user(let id):
            return "\(App.BaseUrl)/user/\(id)"
        case .weather(let day):
            return "\(App.BaseUrl)/weather/\(day.rawValue)"
        }
    }
}

Your router could use this pattern to expose everything from parameters, headers, request types, and more.

And here’s a theme pattern if you need themes:

struct AppThemeModel {
    let baseColor: UIColor
    let backgroundColor: UIColor
    let accentColor: UIColor
    let baseFont: UIFont
}

enum AppTheme {
    case dark
    case light

    var model: AppThemeModel {
        switch self {
        case .dark:
            return AppThemeModel(
                baseColor: .red,
                backgroundColor: .darkRed,
                accentColor: .yellow,
                baseFont: .systemFont(ofSize: 12)
            )
        case .light:
            return AppThemeModel(
                baseColor: .white,
                backgroundColor: .gray,
                accentColor: .blue,
                baseFont: .systemFont(ofSize: 13)
            )
        }
    }
}

// During app init
var currentAppTheme = AppTheme.dark

4. Implementing Data Structures

Implementing trees and linked lists using sum types in Swift is ultra easy:

indirect enum Tree<T> {
    case node(T, l: Tree, r: Tree)
    case leaf(T)

    var l: Tree? {
        switch self {
        case .node(_, l: let l, _):
            return l
        case .leaf(_):
            return nil
        }
    }

    var r: Tree? {
        switch self {
        case .node(_, _, r: let r):
            return r
        case .leaf(_):
            return nil
        }
    }

    var value: T {
        switch self {
        case .node(let val, _, _):
            return val
        case .leaf(let val):
            return val
        }
    }
}

let tree = Tree.node(12, 
    l: Tree.leaf(11),
    r: Tree.node(34, 
        l: Tree.leaf(34),
        r: Tree.leaf(55)
    )
)

Appendix: Enum Syntax Quick & Dirty

Swift enums can be written like this:

enum Foo {
    case a
    case b
}

let sth = Foo.a
let oth = Foo.b

But they can also be written like this:

enum Foo {
    case a(String)
    case b(isEnabled: Bool)
}

let sth = Foo.a("Hello")
let oth = Foo.b(isEnabled: false)