Functors, Applicatives, and Monads in Plain English
This is a no-bullshit zone
Let's learn what Monads, Applicatives, and Functors are, only instead of relying on obscure functional vocabulary or category theory we'll just, you know, use plain english instead.
Functors
Functors are containers you can call map
on. That's it. Seriously.
A million words of category theory and Haskell examples to say Functors contain stuff and if you apply a function to that stuff you get the same kind of container back out with the stuff inside it transformed.
Functors you already know and love include Optional
and all the Swift standard library sequence types like Dictionary
, Array
, and Set
.
To put it another way, a Functor applies functions to the values it contains, instead of to itself. Normally if I call some function doStuff(value: Int)
it requires I pass an Int
. If I have an Optional<Int>
, doStuff
has no clue what to do with that. But since Optional
is a Functor it understands how to take doStuff
and execute it on the value inside itself, then spit out a new Optional
. You don't lose the int-ness or optional-ness.
Know what else can be a Functor? Functions!
// hooray, functions are functors now.
func map<A,B,C>(f1: (B) -> C, f2: (A) -> B) -> (A) -> C {
return { f1(f2($0)) }
}
Swift can't represent Functor in the type system at the moment because it doesn't have "Higher-Kinded Types".
Applicatives
Applicative lets you stuff the function itself inside a container, otherwise it's almost identical to Functors.
I told you we were going to use plain english.
Why? Because you may want to have a function in the container and apply it to value(s) in another container of the same kind. You could extend Optional with apply
like this:
extension Optional {
func apply<U>(f: ((Wrapped) throws -> U)?) rethrows -> U? {
return try f.flatMap(self.map)
}
}
(Some versions of the compiler require dual definitions because it doesn't consider an optional throwing closure as a candidate for rethrows
but I believe that bug is fixed.)
Swift doesn't have a built-in definition for applicative because you can't extend a protocol to say it returns the same container type but with a different element type.
Monad
Monads are containers you can call flatMap
on. Again, that's it.
Why? Because a monad takes special care to avoid double-containering (take that Oxford English Dictionary). flatMap
just says unwrap a value from it's container, apply some function to it, then re-wrap it in a container. If the function spits out the same kind of container just use that instead of wrapping it in a second level container.
Once again, Optional
is in fact a Monad. That's because you can do this:
let value: Int? = 5 // <-- value is wrapped inside a container
// This function has no idea about containers,
// it only wants Ints. It may or may not give us a String.
func sayHello(x: Int) -> String? {
return arc4random() % 2 == 0 ? "hi there \(x)" : nil
}
// That's OK, flatMap will unwrap value if not nil
// and feed it to sayHello. We get a String? back out
// so if value is nil we don't lose our optional-ness either.
let greeting: String? = value.flatMap(sayHello)
We have to preserve the optionality of the result because if our input is nil
the only valid thing we can do is return nil
. We can't sensibly cast nil
to an Int
and call sayHello
anyway. flatMap
saves us from gobs of boilerplate:
let value: Int? = 5 // <-- value is wrapped inside a container
// This function has no idea about containers,
// it only wants Ints. It may or may not give us a String.
func sayHello(x: Int) -> String? {
return arc4random() % 2 == 0 ? "hi there \(x)" : nil
}
// Manually doing what flatMap does
let greeting: String?
switch value {
case .Some(let wrapped):
//if we have a value
switch sayHello(wrapped) {
case .Some(let result):
//function produced a value
greeting = Optional<String>.Some(result)
case .None:
//function produced nil
greeting = Optional<String>.None
}
case .None:
//value itself was nil
greeting = Optional<String>.None
}
Conclusion
It's nice to be able to preserve the "ness" of something, especially when you have containers with multiple states (like Optional) or values inside them (like Collections).
You can take a function that doesn't understand arrays and feed it values one at a time, while still preserving the "array"-ness.
You can take a function that doesn't understand optionals and feed it the wrapped value if there is one, while still preserving the optionality (which comes in handy if it happens to be nil
).
Monads are about taking stupid dumb functions that don't have any clue about containers and applying those functions to the values inside the containers while preserving the containers in the result.
Update: @al_skipp pointed out my original flatMap
example could be improved to better show how flatMap
avoids double-wrapping values in the container. I've updated the post.
Update #2: Some people have interpreted my post as dismissing category theory but that is not my intent. Sometimes boiling a concept down to its simplest concrete implementation is the first step toward greater understanding. For those who want more theory check out Functors, Applicatives, and Monads in pictures or this Swift port of the same article. Learn You A Haskell is a great resource. This WikiBooks article on Category Theory is slightly dense but a good introduction.
This blog represents my own personal opinion and is not endorsed by my employer.
Posted in: genericsswifttype-theory