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 .
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 which for a given type returns the number of possible values that type has. So:
The conclusion that 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 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:
Or in more proper notation:
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 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 and that SumExample contains two Bool types, and .
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:
Expressing a General Case
Let’s assume there exists a type with constituent parts . can be considered a sum type if:
Or in more proper notation:
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:
- 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’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)