Swift Enums-unlocking modelling superpowers

Vagelis Koutkias
XM Global
Published in
14 min readSep 27, 2019

The first time I read about enums in the Swift Programming Guide, I did not even come close to realising their true potential. They seemed like enums from other languages with some additions. But what I did not know back then, was that they would become an invaluable tool in my programming toolset.

Everything clicked a few years ago, when I started learning about functional programming. The emphasis that all FP languages gave on Algebraic Data Types was an eye opener.

Algebraic Data Types

The main concepts behind ADTs are product and sum types. If you are wondering about the naming, yes, they are in a way related to multiplication and addition. What you might have not guessed, though, is that you have been using them everyday.

Product Types

Let’s say we have this following struct:

struct IntBoolProduct { 
let a: Int
let b: Bool
}

As we know, the two properties -a, b- will exist simultaneously in memory for a variable of type IntBoolProduct. So, anytime we instantiate a variable of this type, we need to make sure both properties have a value.

var foo: IntBoolProduct = IntBoolProduct(a: 0, b: false)foo = IntBoolProduct(a: 0, b: true)foo = IntBoolProduct(a: 1, b: false)...

In general, all properties of a struct exhibit this behaviour -they coexist upon the struct’s lifetime- and this behaviour is what makes structs product types. To understand where the naming comes from, let’s think about all the distinct values the type IntBoolProduct can have:

(a: Int.min, b: false)
(a: Int.min, b: true)
...
(a: 0, b: false)
(a: 0, b: true)
...

So the possible values of type IntBoolProduct are those of Int multiplied by those of Bool. In other words, they are the Product of the values of Int and the values of Bool.

What about a class, or a tuple? Under the same assumption, that the properties of a class and the values of a tuple exist simultaneously, they are product types.

Sum Types

Previously, we saw that some everyday constructs we use, are product types. The cool thing is that Swift’s enums come with the associated values addition. This is a game changer versus enums in other languages, because now we can attach values of any type to a case.

enum IntBoolSum {  case a(Int) // The type Int is only tied to the a case  case b(Bool) // The type Bool is only tied to the b case}

Contrary to the product example, the two values -a,b- associated to the cases do not exist simultaneously. At runtime, a variable of type IntBoolSum will either hold an Int or a Bool, but never both at the same time.

var foo: IntBoolSum = .a(0)foo = .b(true)

This ability, to tie a type to a specific case is what makes Swift’s enums sum types. Once again, to understand where the naming comes from, we need to examine all the distinct values of the type IntBoolSum:

(a: Int.min)
...
(a: 0)
...
(a: Int.max)
(b: false)
(b: true)

So, the possible values of type IntBoolSum are those of Int plus those of Bool. Hence sum types.

Algebraic Data Types

Of course we are not constrained to using either a sum or product type. We can build any combination we want. Similarly to doing algebraic operations -additions and multiplications- but on the type level. That’s what ADTs are, combinations of product and sum types. By combinations, we simply mean product or sum types containing other product or sum types.

An example from our codebase:

struct AutoLoginError { // AutoLoginError is a product type  enum `Type` {    case keychainError    case networkError(NetworkError)   }  let type: Type // AutoLoginError.Type is a sum type  let message: String
}

As we will see later on, this exact ability, to combine easily ADTs, is one of their true strengths. In case you want to calculate the possible values of the “combined” type AutoLoginError, add or multiply the possible values of the contained ADTs until you reach to primitive types. Don’t worry, we will see an example of this later on.

Making illegal states unrepresentable

Why does all this matter? Because, with carefully crafted ADTs, we can express naturally our domain. They provide a guide we can follow when trying to model a solution. All we need to do is ask ourselves:

Does it make sense for some properties to exist at the same time? Use a product type. Or does the domain demand that at any time we should have either one case or the other? Then, use a sum type. And as we saw previously, we can also combine them. To illustrate the thought process and why choosing correct types matters, let’s go through an example.

A known limitation of using exceptions as error handling mechanism, is that they cannot be used in asynchronous callbacks. As a way to overcome this, you might have seen something like this:

func getUser(completion: (user: User?, error: Error?) -> Void) {  ...
}

This code tries to express the possibility of failure with two optionals, one for the success and one for the error scenario. Unfortunately, there are a couple of issues with this solution. Think about the distinct values the closure parameters can have:

What about the highlighted values? Should our code even handle them? Should we write tests about them?

getUser { (user: User?, error: Error?) in  if let user = user {    // What should we do if the error also has a value here?   
// Should we assert?
// What if we had the if let error = error case first and then the other case?
// Should the order matter?
} else if let error = error {
}}

These values (User, Error), (nil, nil) are “illegal” states. We hope that they will never appear at runtime. The important question is, should our program or a library that we use even be allowed to create them?

Let’s try another approach. We will think about the behaviour we want and start building an incremental solution with ADTs.

  • The closure should complete with either success or failure: (hint: Sum Type)
enum Result {  case success  case failure}
  • Only at the success case does it make sense for the user value to exist: (hint: User should be an associated value to the success case)
enum Result {  case success(User)  case failure}
  • Similarly, the error value is only meaningful in their failure case: (hint: Error should be an associated value to the failure case)
enum Result {
case success(User)
case failure(Error)
}

So what are the possible states in the completion closure now? It is either Error or User. We eliminated the two invalid states of the previous approach. Not only do we not need to handle them any longer, more importantly, we have made it impossible to create them. And in case we had written tests for the “illegal” states, we can delete them.

(You probably noticed, that what we ended up with, is similar to Swift’s Result type, only simpler. We “fixed” the success generic type parameter to User.)

Modelling our specs into types

The nice thing is that the more we start modelling the problem with ADTs, the more our code starts reflecting our specs.

The other day we had a code review with a new member of our team. In our app we had a Settings object which amongst other properties, contained two Booleans hasRememberMeEnabled, hasPasswordTouchIDProtected. He was confused about the meaning of those. And he had every right to be.

What was the spec? The Login screen contains two switches for rememberMe, and TouchID protection of the password. The TouchID switch can only be on when the rememberMe switch is on (if rememberMe is disabled the app does not save the password). So the app needs to ensure this consistency across user actions. Let’s illustrate this with a gif:

And because the gif might be a bit confusing, let’s clear this out:

The problem with the boolean representation is obvious. It allows an illegal state (isRememberMeEnabled: false, hasPasswordTouchIDProtected: true). Even worse, nothing indicates the association between these two properties. It is up to us to remember to update both of these properties accordingly.

Once more, let’s try to express the spec in our types using ADTs.

  • The rememberMe can either be enabled or disabled.
enum RememberMeOption {  case disabled  case enabled}
  • Only while at the rememberMe enabled state, does it make sense to talk about whether the password should be protected with touchID or not.
enum RememberMeOption {  case disabled  case enabled(isTouchIDEnabled: Bool)}

Now the specs are depicted in our type. It’s clear how these two options are associated and what they mean. Updating them is simple and we cannot create any illegal states. We managed to move the cognitive load into the type.

The compiler at our disposal

Another thing to keep in mind, is that sum types come with a very powerful assistant. While switching on the possible values of an enum, the compiler makes sure we have handled them all. How? Pattern matching needs to be exhaustive for the code to compile.

Think about the previous completion closure example. In the first approach (User?, Error?) we discussed about whether we should handle 2 or 4 of the possible scenarios. Either we handled one or all of them, the compiler could not give us a hint if we left something out. In our Result enum solution, the compiler makes sure we handle both of the possible execution paths or it will fail with an error.

Exhaustive pattern matching can prove useful during development phase, because it forces you to handle all possible scenarios. But where it really shines, is future proofing our code.

In our codebase we have been using enums for modelling possible errors of an operation. One example:

Let’s say we needed to add another type of error e.g. databaseError:

All of the places in our code base, where we switched over the specific error type, will fail to compile. We have to go through one by one and think about what our app needs to do for this extra case.

Compiler has our back.

A word of caution here for using default while switching, because it works as a catch-all pattern. Once we add default on a switch, we remove the safety net, and any new cases we add, will be handled by the default clause. No compiler errors to warn us.

A powerful reasoning tool

Now let’s go back to the distinct values analysis we mentioned in the beginning. Its usefulness goes way beyond just explaining the naming of sum and product. It provides a powerful way to reason about our types. When we see a type, we can break it down to its distinct values and think about what it represents.

Let’s use once more the previous RememberMe example. In the end we arrived at this representation:

enum RememberMeOption {  case disabled  case enabled(isTouchIDEnabled: Bool)}

What are the possible values of the RememberMeOption type?

Note: The possible values of an enum case without an associated value = 1

RememberMeOption possible values = 
1 + (The values of type Bool) =
1 + 2 =
3

What if we used the below representation instead? Do we create any “illegal” states?

enum RememberMeOption2 {  case disabled
case enabledWithTouchIDEnabled
case enabledWithTouchIDDisabled
}

The possible values of this approach are:

RememberMeOption2 possible values = 
1 + 1 + 1 =
3 =
RememberMeOption possible values

Does this equality mean something? It means that those two representations are equivalent from type perspective. Types like these, which have the same possible values, are called isomorphic. This means that you can always transform from one type to the other, without losing any information. In other words, you can always create the following two transformation functions:

func rememberMeOptionToRememberMeOption2(_ rememberMeOption: RememberMeOption) -> RememberMeOption2 {  switch rememberMeOption {    case .disabled:      return .disabled    case .enabled(let isTouchIDEnabled):      return isTouchIDEnabled ? .enabledWithTouchIDEnabled : .enabledWithTouchIDDisabled  }}func rememberMeOption2ToRememberMeOption(_ rememberMeOption2: RememberMeOption2) -> RememberMeOption {  switch rememberMeOption2 {    case .disabled:      return .disabled    case .enabledWithTouchIDEnabled:      return .enabled(isTouchIDEnabled: true)    case .enabledWithTouchIDDisabled:      return .enabled(isTouchIDEnabled: false)  }}

Noticing that two types are isomorphic, gives us an important hint. That both types can be used interchangeably and our choice can depend on other criteria, like clarity, ease of use, extensibility, etc.

How do we choose between the two? In this case we prefer the first approach because it conveys the spec better. The association of the touchID protection with the remember me option is much clearer.

And now on to a more interesting example. While we were refactoring the login/logout functionality of the app, we wanted to model the reasons that can trigger a logout:

enum LogoutReason {  case autoLoginFailed(message: String, loginError: LoginError)  case logOutButtonPressed(message: String)}

Notice that the message associated value repeats in all cases. We discussed whether it would be better to extract message to a struct like this:

struct LogoutReason2 {  enum `Type` {    case autoLoginFailed(loginError: LoginError)    case logOutButtonPressed  }  let type: Type  let message: String}

Let’s break both approaches down by doing the distinct values analysis and see what we come up with:

Note: For simplicity we will name:

A = The values of type String

B = The values of type LoginError

Reminder: The possible values of an enum case without an associated value = 1

LogOutReason possible values = (A x B) + ALogOutReason2 possible values = 
(B + 1) x A =
(A x B) + A
LogOutReason possible values

Or we need not have done this process at all, had we noticed that the two approaches are equal, through the application of the distributive math property:

where the a in our case was the repeated message value

Cool, right?

So since we understood that these two types are isomorphic, we know for a fact that we are free to choose one over the other.

How do we choose between the two? In the above scenario, we prefer the second approach, with the struct grouping the common values. This way, we guarantee that the common fields will be available for all cases -even future added ones- and they are easy to access. On the other hand, the top level enum approach, would require switching to access the common message value, even though we know that all cases contain it.

This example might have looked trivial even before introducing ADTs and isomorphism, but it is powerful to be able to reason about your types and weigh your choices.

Hitting on a roadblock

So far, we have laid out many of the advantages of ADTs. However, if you keep using them consistently, you are bound to face some difficulties.

Trying to express a problem precisely, might result in complicated ADTs (complex combinations of enums and classes/structs). Especially dealing with deeply nested enums is hard, as in order to access a value you might need to go through multiple nested switches. The pattern matching I so highly previously appraised, starts to become a burden. Accessing or updating a value requires multiple lines:

However, this doesn’t mean that in such cases we should dismiss precise modelling. On the contrary, we are probably facing a complicated problem. Putting it down to correct types should make things clearer. All we need is better mechanics. One solution, would be to expose optional access properties for the cases of the enum. So the previous code could be reduced to this:

The Pointfree team has some nice suggestions how you can supercharge your enums with such functionality and even more.

Additionally, in functional programming, there is an approach called optics (warning: not a very beginner-friendly topic) that could prove handy when dealing with complicated ADTs and compositionally. As someone might guess, these problems have already been faced by other communities, so we have somewhere to look when things get rough.

A wealth of knowledge

After all, that’s another nice thing about and ADTs. They have long existed in some languages and most of the modern ones -Swift, Scala, Kotlin- have a way of representing them. There is a lot of material we can learn from.

I attach a glossary table of some popular languages and how sum types can be represented there, as a head start for anyone interested to do some research outside Swift context:

Unfortunately, in languages that do not directly support sum types, expressing them is a bit hard. We can use the Visitor pattern which is a bit cumbersome. Or use product types with smart constructors and closures to emulate exhaustive pattern matching. You can check a sample of this in Pinterest’s Plank library (in the Algebraic Data Types section). They are using code generation to avoid the repetitive boilerplate needed to emulate sum types.

Maybe once you have used sum types, going back to not having them is not option. That’s at least how I feel. What first appeared as an indifferent addition to the language, turned out to be the feature of Swift I cannot code without. And because this does sound like an exaggeration, I attach a note from the ELM official language guide: (custom types in Elm are essentially an easy way to represent ADTs)

References - Further Reading

The amazing PointFree team from the iOS community in their video series explores functional programming and Swift. Related to this post you can check the Algebraic Data Types episode and the videos on Enum properties (4 episodes).

In his site, Scott Wlaschin aims in bringing the joys of functional programming to mainstream development. It offers very well written pieces of functional concepts and the author tries to make them accessible to the OOP programmer. Related to this post, check the Designing with Types series.

Elm is a pure functional language and with its Custom types it can easily express ADTs. The community has to showcase several examples of precise modelling.

A talk by Richard Feldman in ElmConf showing how you can design your models to make impossible states impossible at compile time.

LambdaCast is a podcast discussing functional concepts in everyday language. They have a whole ADTs episode.

A book by Debasish Ghosh focused on domain modelling from the functional programming perspective, which also has a section on ADTs.

A book by Bartosz Milewski on Category Theory. It has a chapter about ADTs and the related concepts from the Category Theory point of view.

This is a post from Mark Seemann’s personal blog (highly recommended in general). In this post, the author showcases how you can use the visitor pattern in a language that does not support sum types, to achieve the same objective.

Plank is an iOS library by Pinterest for model generation. In the linked medium post, in the Algebraic Data Types section, you can see how the Pinterest team emulates sum types in Objective-C

Special thanks to George Sotiropoulos, Nikos Linakis, Kostas Kremizas and the amazing WebTrader guys for contributing and proofreading.

--

--