Monad Magic

Jonathan @ Strictly Swift
9 min readJul 6, 2018

--

Source: British Library / openclipart.com

So far we have looked at some powerful tools with the HKT framework — Functors and Applicative Functors. We’re going to go one step further in this article and look at the most powerful tool yet — Monads.

Then, we’ll look at some examples of Monads, including Arrays, Optionals and WebData. A companion article to this will look at some more: Writers, Readers and Futures.

Check out the Github page for more!

Transforming WebData 🔰

We’re going to continue the example from last time, and talk about WebData<A> which represents a value we get from the internet (eg, someone entered it into a form). Because we don’t trust data from the web, we can encode it as tainted. Data we have decided we do trust we can annotate as safe.

So suppose we get email addresses from the web, and run them through a Validator to confirm if they are safe or not:

let emailInput = WebData("bjones@email.com", withValidator: emailValidator)
let dodgyInput = WebData("fak3N@m3", withValidator: emailValidator)

We also have a function to let us greet the user:

let greeter: (String) → WebData<String> = 
{ return WebData.safe("Hi \($0)!") }

How can we use that greeter function with those addresses? Note that greeter always returns a “safe” result, but we might not want that: we don’t want to accidentally turn our dodgyInput into a safe one. We therefore want a function like this:

func greetSafely(_ name: WebData<String> ) → WebData<String> {
switch name {
case .safe(let n): return greeter(n)
case .taint(let n): return greeter(n).taint()
}
}

…in other words, we make sure the result maintains the original taintedness of the input name .

Let’s try it:

greetSafely( emailInput )  // "Hi bjones@email.com!"
greetSafely( dodgyEmailInput ) // "TAINTED: [Hi fake3N@m3!]"

That works, but obviously is tied to the greeter function. What about making this more generic? We’d want to do two things:

  • allow other types of WebData, not just Strings
  • allow any transform function with the pattern A → WebData<B> to be used, not just greeter

Let’s therefore make this an extension on WebData itself, so we can pass in the transform function. As before, we need to be a bit cautious about how we apply the transform, to make sure we always return a tainted result:

extension WebData {
func applySafely<B>(_ transform: @escaping (A) → WebData<B> )
→ WebData<B> {
switch self {
case .safe(let a): return transform(a)
case .taint(let a): return transform(a).taint()
}
}
}

So, if we have another function to find a user’s name from their email address (if they are in our system):

func findUserName(withEmail:String)-> WebData<String> { ... }

…we can get a friendlier greeting:

emailInput.applySafely(findUserName).applySafely(greeter) 
// "Hi Bob Jones!"
dodgyInput.applySafely(findUserName).applySafely(greeter)
// "TAINTED: [Hi unknown!]"

We can make this friendlier for the developer too, by defining an operator. For reasons we’ll go into later, we will call it bind and use >>>= as the symbol:

func >>>= <A,B>(l:WebData<A>, @escaping r:(A) → WebData<B>) → WebData<B> {
return l.applySafely(r)
}

Now, we can chain our functions on WebData nicely:

emailInput >>>= findUserName >>>= greeter  
// "Hi Bob Jones!"
dodgyInput >>>= findUserName >>>= greeter
// "TAINTED: [Hi unknown!]"

Getting to Monad 🌮

Let’s look at the function type of applySafely again:

func applySafely<B>( transform: (A) → WebData<B> ) → WebData<B>

You might notice that it is very similar to another function:

func flatMap<B>( transform: (A) → Array<B>) → Array<B>

This is not a coincidence. flatMap on an Array applies a transformation which turns an array into an “array of arrays” . It then “flattens” the result back into a single array : “looking through” each inner array and pulling out the contents into the outer array.

For example, a function let twice = { [$0,$0] } will transform [1,2,3,4] with regular map to [[1,1],[2,2],[3,3],[4,4]]. However, flatMap will then “look through” into the inner arrays like [1,1], and pull the contents out, to give us [1,1,2,2,3,3,4,4].

applySafely as flatMap

Let’s think of applySafely in the same way. First it applies the transform function to the contents of the WebData container. Just doing that will give a WebData<WebData<B>> : ie a “container of containers”.Then the function then pulls out the value contained in the inner WebData, makes it tainted if necessary, and puts the result into the top level WebData object.

So applySafely is actually performing a very similar operation to flatMap, and it is helpful to think of them of being the “same kind” of function. This is a very powerful idea which we’ll start to see in many places. For instance, note that a “container of containers” WebData<WebData<X>> is not a useful idea (the dual wrapping isn’t helping us represent "more tainted" - what would that even mean?); nor is Optional<Optional<X>> (something is “more optional”?) ; and the same is true for many other types. Being able to transform the contents of a container into sub-containers, but then pull out those inner values back to the top level container is often very useful.

This is the key idea behind a Monad.

It’s worth noting that in many languages, flatMap gets another name – bind. I’ve stuck with that tradition in the HKT library.

So… what’s a Monad? 📬

The point of this is that Monads are just types which declare flatMap a.k.a. bind. In other words, any type M that has a definition like this¹ is a Monad:

func bind<A>( transform: (A) → M<B> ) → M<B>  { ... }

You will also need to define a function called pure, which just returns a value wrapped into the Monad:

static func pure<A>(_ a: A) → M<A>

Note if M is also an Applicative Functor, this is exactly the same definition, so no need to re-define it.

Another way of putting that is that Monad means Flatmappable or flattenable — a type which declares flatMap is essentially a Monad.

Monads and HKT

To make your own type into a Monad using the HKT framework, you just need to have it conform to the Monad protocol, which means you need to implement bind (and pure, if the type is not already an Applicative functor).

You will then get the operator >>>=defined for your type, which looks² like:

func >>>=(left: M<A>, right: (A) → M<B>) → M<B>

Another way of thinking about Monads

The above discussion considers Monads as “flattenable” containers. Another way of gaining some intuition about Monads is to think of them as computations you can combine .

Look closely at the types of >>>=. This operator combines computations: M<A> is the first computation. If the second argument was just M<B>, then there wouldn’t be any way for the first computation to interact with the second computation (this is actually the same as Applicative functors).

However, the second argument is in fact(A) -> M<B>, which means we can use the result (ie, A) from the first computation to decide what happens in the second computation. Hence Monads are more powerful than Applicative functors; they can use the result from a previous computation to figure out what computation to run next.

Familiar Monads 🎭

Now we have seen a bit of what they are for, let’s look how some familiar types become Monads.

It might help to look at the github code to see more examples.

Optional

As you may know Optional already comes with a flatMap function, which converts types like String?? into String? (or Optional<Optional<A>> to Optional<A>). We can use that to define bind for Optionals:

public func bind<B>(_ m: @escaping (Wrapped) → B?) → B? {
return self.flatMap { m($0) }
}

This can help us avoid a lot of wrapping/unwrapping of Optionals, and optional chaining. Swift already makes a lot of working with Optionals really nice with built in ? syntax, and looking at Optional as a Monad gives us even more power.

For example, suppose we have a list of Strings, possibly representing numbers, and we want to check if the first value in the list is even. If the list is empty, or if the first value is not an even number, then return nil.

let list : [String] = ["1.5", "1", "2.5", "2"]

We can use a couple of helper functions:

let firstValue : ([String]) -> String? = { $0.first }
let toInt : (String) → Int? = { Int($0) }
let isEven : (Int) → String? = { ($0 % 2 == 0) ? "even" : nil }

Using our Monad bindings, we can implement this as:

let firstValueIsEven = list >>>= firstValue >>>= toInt >>>= isEven

…without an optional-unwrapping ? in sight!

It’s interesting to look at what this might look like using if let optional binding, the usual way you’d code this in Swift. Clearly this is rather more verbose, and relies on a mutable var variable:

var firstValueIsEven : String? = nil
if let firstValue = firstValue, let maybeInt = toInt(firstValue) {
firstValueIsEven = isEven(maybeInt)
}

Array

Array is a Monad (bind is just flatMap, in fact the implementation is basically identical to that for Optional). We’ll use this section to go through a slightly more complex Monad example (thanks to Alexandros Salazar for this suggestion!)

Suppose we are running a chess tournament in 3 different cities. We want to produce the roster of all the different games, where every competitor must play everyone else.

First, let’s try this with regular Array functions.

func teamForCity(_ c: String,_ t: Team) → [String] { ... }

func matchupsForCities(cities:[String]) → [Matchup] {
var matchups = [Matchup]()
for city in cities {
let teamA = teamForCity(city, Team.A)
let teamB = teamForCity(city, Team.B)
for p1 in teamA {
for p2 in teamB {
matchups += [Matchup(city:city,
player1:p1,
player2:p2)]
}
}
}
return matchups
}

Here, we’re explicitly creating 3 nested for loops, and our logic (the Matchup line) is drowned out by the boilerplate.

But thinking about the logic of what we are trying to do, this is an example of flatMap. For every city, and every team within that city, we are “reaching in” to the players in that team and pulling out the players into a top-level Matchup. So, let’s try this using our new version of flatMap, operator >>>= :

func matchupsForCities2(cities:[String]) → [Matchup] {
return cities >>>= { city in
teamForCity(city, Team.A) >>>= { p1 in
teamForCity(city, Team.B) >>>= { p2 in
[Matchup(city:city, player1:p1, player2:p2)]
}}}
}

We can read the above like this:

  • Think of >>>= as gluing (binding) a Monad value M<A> (ie, a container with a value inside like cities) to a closure/function A -> M<B>
  • The glued/bound closure (or function) inspects the contents of the container, binding each time to a variable (eg city)
  • Operations (eg teamForCity) can then be done on that variable - those operations can return further Monad values (the result of teamForCity is another Array)
  • Further drilling into the result of that operation can be done by gluing via >>>= to another closure (eg to p1 )
  • It’s also possible to refer back to the contents from an earlier operation — for example, the teamForCity(city, Team.B) refers back to the contents of the cities monad.
  • At the end of all the binding operations, the results all get wrapped up into a “top level” container — in this case an Array.

Admittedly this may take a little practice to read, but even a quick glance now shows the structure of the problem — a 3-level unwrapping, from city to team A and from city to team B, and finally to a matchup.

ZipArray

ZipArray we defined as an Applicative Functor is also a Monad, and performs the same flatMap as per a regular Array (no “zipping”).

WebData

We already saw that WebData is a Monad. We can just re-write our applySafely function as bind:

func bind<B>( transform: @escaping (A) → WebData<B> ) → WebData<B> {
switch self {
case .safe(let a): return transform(a)
case .taint(let a): return transform(a).taint()
}
}

What’s Next?

This was an introduction to a very powerful way of creating software by “gluing” — binding — together smaller functions. It may be hard to see how you might use monads in your code, so next time we’ll introduce some more sophisticated examples to show the power of this pattern.

¹Monads also follow “Monad laws” which I’ll not describe here; though most well-designed types would follow these naturally.

²Languages like Haskell use >>= as the operator, but Swift already has a built-in operator >>= (“right bit shift and assign”) and the compiler gets confused if we try to overload it! Hence >>>=. It’s pronounced “bind.”

--

--