Swift Enums Are 'Sum' Types. That Makes Them Very Interesting
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 arent new types. They are simply a new way of thinking about the types you already know about.
There are many different algebraic types - in fact - all of the types you currently use are algebraic as well. Here, we’ll two basic algebraic types
- Product types
- Sum types
So let’s start with the familiar stuff.
Product types
Product types are nothing more that Swift struct
types, or Java class
types.
They are 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
}
So 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 which is
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 possible values, Bool
has possible values. So our ProductExampleTwo
has
possible values.
In general, without going into type theory notation, we can define a function which for a given type - returns the number of possible values that type has. So:
\[ N_{pv}(Bool) = 2 \] \[ N_{pv}(Int8) = 256 \] \[ N_{pv}(String) = \infty \]
The conclusion that has certain implications in the way we think about types, but those can be omitted for the current article without compromising the general message
If we use the function - we can express a general case for all product types.
Let’s assume there exists type which has constituent parts . can be considered a product type if:
\[ N_{pv}(T) = N_{pv}(T_1) \times N_{pv}(T_2) \times N_{pv}(T_3) \times … \times N_{pv}(T_n) \]
Or in a more proper notation:
\[ N_{pv}(T) = \prod_{i = 1}^{n}N_{pv}(T_i) \]
Sum types
If you’re, by any chance, unfamiliar with enum syntax - there is a quick and dirty intro at the bottom of the article.
So if struct
-s were product types - then what are sum types?
Simple - Sum types (in swift) are enums!
Number of possible values of a sum type is the sum of the number of possible values of it’s constituent parts
Now that we know how to deal with the algebraic view of types, and have our 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 ways to instantiate the SumExample
enum. This number comes
from the fact that and that SumExample
contains two
Bool
types and that
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
\[ N_{pv}(Bool) = 2 \] \[ N_{pv}(Int8) = 256 \] \[ N_{pv}(SumExampleTwo) = N_{pv}(Bool) + N_{pv}(Int8) \] \[ N_{pv}(SumExampleTwo) = 2 + 256 \] \[ N_{pv}(SumExampleTwo) = 258 \]
Expressing a general case:
Let’s assume there exists a type with constituent parts . can be considered a sum type if:
\[
N_{pv}(T) = N_{pv}(T_1) + N_{pv}(T_2) + N_{pv}(T_3) + … + N_{pv}(T_n)
\]
or in a more proper notation:
\[ N_{pv}(T) = \sum_{i = 1}^{n}N_{pv}(T_i) \]
How can I use it to write better code?
All this theoretical stuff is fine and dandy - but let’s see a little bit of practical examples.
So 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 you’re 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:
- Success - result was fetched from the server
- 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 are actually
counting only on two cases the 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 an inbuilt 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 a let a: String? = "Hello"
wold just be shorthand syntax for
let a = Optional.some("Hello")
.
Good thing is that swift has some neat syntax sugar to help us out on our
distinguishing of 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 for example 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, etc…
And now a theme patten 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: .systemFontOfSize(12)
)
case .light:
AppThemeModel(
baseColor: .white
backgroundColor: .gray
accentColor: .blue
baseFont: .systemFontOfSize(13)
)
}
}
}
// During app init
var currentAppTheme = AppTheme.dark
3. Implementing data structures
Implementing trees and linked lists using sumt 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: // equivalent implementation to l
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)))
Enums 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 can be also written like this:
enum Foo {
case a(String)
case b(isEnabled: Bool)
}
let sth = Foo.a("Hello")
let oth = Foo.b(isEnabled: false)
Mislav Javor
I'm an entrepreneur and a software developer. CEO of a blockchain startup aiming to simplify buying and selling of electricity. Actively participating in the proliferation of healthy (technology oriented) blockchain culture. Organizer of Blockchain Development Meetup Zagreb, lectured at HUB385 Academy and University of Osijek on topics of smart comtract development. In my free time, I'm a singer and a guitar/piano player. Contact at mislav@ampnet.io