Rob Napier has been to Monad, to the Functor of Doom. He has seen the map, flattened and lensed. He has folded the infinite, lifted a Maybe, and he would do it all over again. But from what he has seen, from Haskell to Church, we can rely on one truth, which is this: Swift is not a functional programming language. Pushing too hard to make it one fights Swift and breaks Cocoa.
However, Swift has absorbed some fantastic lessons from the functional world, and while value types may not quite be the present, they are clearly the future. Rob explores how decades of work in functional languages have influenced Swift, and how you can use those features best while staying true to Swift, playing nice with Cocoa, and embracing Protocol Oriented Programming.
Introduction (0:00)
A week after Swift was first announced, I wrote a blog post called “Swift Is Not Functional”. Two years later, Swift is still not functional. Instead of expounding on that, this post will instead discuss the decades of experience and research that have gone into functional languages, which we can bring to Swift in our own Swifty way.
What is functional programming?
It is monads, functors, Haskell, and elegant code.
Functional programming is not a language or syntax - it is a way of thinking about problems. Functional programming was around for decades before we had the monads. Functional programming is a way of thinking about how you tear down problems and then put them back together in structured ways.
Let’s start with a simple example in Swift.
var persons: [Person] = []
for name in names {
let person = Person(name: name)
if person.isValid {
persons.append(person)
}
}
It is a simple loop, it does two things: it converts names into persons and then puts those onto an array if they are valid. Really simple, but there is much going on here. In order to understand what is going on, you need to execute this in your head. You need to be thinking, “What does this array do?”
It looks simple, but it could be simpler. We could separate the concerns. We could pull things apart.
We now have two loops, and each loop does less. Each one is simple, and simpler code lets us find patterns: create an array, loop over some values, do something to each value, and then at the end put those onto another array.
When you have something that gets done many times, you probably should extract the function - Swift has one, it is called “map”. Map converts a list of something into a list of something else. The important thing is that it says what it means: possible persons is the mapping of names
to person
. It is a list of persons based on their names. We do not have to execute anything in our head, it is right there in the code what we mean.
let possiblePersons = names.map(Person.init)
let persons = possiblePersons.filter { $0.isValid }
The other loop is also a very common pattern: it has a method called “filter”. Filter takes a predicate, which is a function that returns a bool. It applies it and gets us the ones that are valid. We can put these together and we can have possible persons and then tie them into our filter.
We got from seven lines of code down to two lines of code. Plus, we can recombine them: these are all values we can put them back together. We put them together with chaining.
It’s very easy to read, and we can read it one line at a time.
let persons = names
.map(Person.init)
.filter { $0.isValid }
It is good to learn it and start to become comfortable with it (in my opinion this is excellent Swift). You should not write a for-loop in this case.
Functional Tools (5:54)
In 1977, John Backus (who helped invent FORTRAN and ALGOL) won the Turing award, and gave the lecture “Can programming be liberated from the von Neumann style?” I love this title. “Von Neumann style” means FORTRAN and ALGOL.
This paper was his apology for inventing them. He means imperative programming, the step by step mutation of some state until you get into the final state you want to be in. And when he says “functional” he does not mean what we mean by functional today, but he inspired many functional researchers to go and study.
This paper interested me because of something we can take back to Swift: how we can decompose complicated things into simple things. Make those simple things generic and then glue them back together using some rules, like algebra.
Algebra is a set of rules for putting things together, tearing them apart, and transforming them. We can come up with rules we could use to manipulate programs. We already did that: we took a loop, we decomposed it into two simpler loops, found generic forms of each of them and then put them back together using chaining. When Haskell programmers come over and they encounter Swift for the first time, they tend to get frustrated because they try to do what you can do in Haskell.
In Haskell, like pretty much all functional languages, the basic unit of composition is the function. There are beautiful ways to compose functions. I can create a new function, called sum
, by gluing together the foldr
function, the +
function, and giving it initial value of 0
. Whether this is comfortable for you to read or not, once you do, it is quite beautiful.
let sum = foldr (+) 0
sum [1..10]
You can do this in Swift, but it is ugly, and it does not work well because you are working on the wrong unit of composition. Swift is not functional.
The unit of composition in Swift is the type. Classes, structs, enums, and protocols, these are all things that you can compose with. We commonly glue together two types. By implementing one function and gluing them together, we compose things by building them up out of simpler pieces.
Another kind of composition in Swift that is very common is lifting the type into a context. The most common form you are used to is optionals. An optional is a type lifted into a context, and that context maybe has a value, maybe it does not. That is a little piece of information that goes along with the type: does it exist? That is what context means. Adding context is much more powerful than other ways of tracking that extra information.
extension MyStruct<T>: Sequence {
func makeIterator() -> AnyIterator<T> {
return ... }
}
One way we could have tracked the fact that there is no integer, no value at all, is that we would have stolen one of the values out of integer like -1
, which means there is no value. But now you have to test that all over the place; that is ugly, error-prone, and the compiler cannot help you.
If we lift integers into an optional, the compiler can help you. You have this context of “does it exist, does it not exist, and I can help you make sure you do not forget about that.” If you have ever used a -1
, it is easy to forget to check, and then all of a sudden your program goes crazy.
Example (11:44)
Let’s build up a more complicated example.
func login(username: String, password: String,
completion: (String?, Error?) -> Void)
login(username: "rob", password: "s3cret") {
(token, error) in
if let token = token {
// success
} else if let error = error {
// failure }
}
This is a very common API. We have some function login
that takes a username
and a password
, which at some point is going to pass back a token
and a possible error. I think we can do much better by thinking about the context.
The first problem is this completion
block. I say it is a String, but what is that string? It is a token. I could throw a label on it, but that does not help. In Swift, labels are not part of the type. And even if I did that, it still is this string. Strings are many things.
Tokens have rules: maybe they have to be a fixed length, for example, or they cannot be empty. Those are things you can do with strings, but not with tokens. We want to have more context about token
; it has rules, so we want to capture those rules. We can do that, we can give it more structure. That is why it is called struct
.
struct Token {
let string: String
}
I take my string and I throw it in a structure. This does not cost anything, and does not cause indirection or any extra memory usage, but now I can put rules on this.
You can only construct these with certain strings. I could put extensions on it that would make no sense to stick on arbitrary strings. This is much nicer, and I can do this with all types; strings of course, but also dicts and arrays and ints.
When you have these types, you can lift them into a context and control what you can put onto them. You can control what they mean. I do not need labels or comments, because it is clear that the first parameter is a token because its type is Token
.
The second problem is we are passing a username
and a password
. In most programs that have these, you always pass them together; the password is particularly useless by itself. I would like to create a rule that allows me to compose usernames and passwords with “and,” so I need an “and” type. We have one, and it is a struct again.
“AND” Type (Product) (14:50)
struct Credential {
var username: String
var password: String
}
Structs are “and” types. The Credential
is a username
and a password
. The common name for “and” types is “product type”.
I encourage you to say stuff out loud. For instance: “A credential is a username and a password.” Does it make sense? If it does not make sense, maybe it is the wrong type, or maybe you have built it wrong.
func login(credential: Credential,
completion: (Token?, Error?) -> Void)
let credential = Credential(username: "rob",
password: "s3cret")
login(credential: credential) { (token, error) in
if let token = token {
// success
} else if let error = error {
// failure }
}
Now we can swap in Credential
rather than username
and password
: it also makes our signature shorter and clearer. We also open up many nice possibilities: put extensions on Credentials, or swap in other types of rules for them. Maybe we want one0time passwords, or access tokens, or Facebook or Google, etc. Now, I do not have to change any other part of the code because I am just passing credentials.
Still, it has problems. We have passed this tuple of (Token?, Error?)
– tuples are “and” types. They are anonymous structs. Do we mean “maybe a token and maybe an error”? There are four possibilities there: both, or neither, or one or the other. Only two of those possibilities make any sense. What if I did get both a token and an error? Is that an error condition? Do I need a fatal error there? Do I need to ignore it? You need to think about that and probably write tests against it.
“OR” Type (Sum) (17:19)
The problem is you do not mean “maybe” anything - you mean a token or an error. Do we have an “or” type we can use?
enum Result<Value> {
case success(Value)
case failure(Error)
}
It is an enum - enums are “or” types (this or that), whereas structs are “and” types (this and that). Like “and” types are called “product types”, “or” types are called “sum types”.
func login(credential: Credential,
completion: (Result<Token>) -> Void)
login(credential: credential) { result in
switch result {
case .success(let token): // success
case .failure(let error): // failure
}
}
I want to build this little result
type. This really bothers me that it is not built into Swift. It is easy to build. We are going to lift our value in to give it more context. It goes from being just a value to being a successful value.
Our error becomes a failing error, and we have more context. If we throw that result, that resulting token into our API, all of those impossible cases that we were having to write tests against all go away. We do not have to worry about them because they are impossible. I want the bugs to be impossible rather than write test cases.
I like this API. I log in with a credential, and it will give me back a resulting token.
The Lessons (18:51)
That is the true legacy of functional programming, and the piece we should bring to Swift: Complicated things can be broken down into smaller, simpler things.
We can find generic solutions for those simple things, and we can put those simple things back together using consistent rules that let us reason about our programs. This lets the compiler make bugs simply impossible, and that is something I think that John Backus in the 70s would completely agree on. Break it down, build it up.
Receive news and updates from Realm straight to your inbox