The Culmination: Final Part

In my last post, I finally came out and admitted that all my posts about errors and optionals have been about monads. And to justify this gross act of deception, I posited for-comprehensions, a nifty new syntax in Magical Future Swift that improves how we can work with optionals and arrays—a syntax that only works with monads.

So monads are entities that work with for-comprehensions. But for-comprehensions must work in a sane manner, that is, intuitively. I’ve already gone over two behaviors that are required; they are called the first two monad laws. There is one missing. You’ll be stunned to know it is the third law.

• • • • •

The first law addressed using a for-comprehension with a lifted value. The second law addressed the behavior of comprehensions when nothing was done to the assigned values. One last thing we want to be able to do with comprehensions is nest them.

To see why, suppose we have a function that has side effects with type signature doSomething() -> Int?. We call this function to do things for us, and we potentially get a number back. Here’s something we might want to do with this function:

func doSomethingTwice() -> Int? {
    return for {
        ignore <- doSomething()
        b <- doSomething()
    } yield {
        b
    }
}

In effect, we call the function twice, but we only care about the result the second time. There is no need to do this with a comprehension, of course, but if comprehensions are available, it must work, and the whole point is to discover what monad laws make comprehensions intuitive. Now, let’s use this function:

let a = for {
    b <- doSomethingTwice()
    c <- process(b) // process(val:Int?) -> Int?
} yield {
    c
}

We can substitute the definition of doSomethingTwice:

// Form 1
let val = for {
    b <- for {
             ignore <- doSomething()
             b1 <- doSomething()
         } yield {
             b1
         }
    c <- process(b) // process(val:Int?) -> Int?
} yield {
    c
}

I call it “Form 1” because we know that the return value of the first call gets discarded (though still made), which means we should be able to write that snippet like this:

// Form 2
let val = for {
    ignore <- doSomething()
    b <- doSomething()
    c <- process(b) // process(val:Int?) -> Int?
} yield {
    c
}

Let me be clear. When I said “which means” above, I meant that it made intuitive sense. There is nothing that guarantees that this is the case. It’s our job to guarantee it—and that is the role of the third monad law.

The the third law follows from turning these two different forms into their >>=- versions. Doing so is nothing overly complicated, but it is a bit lengthy, so I’ve put the details in an addendum. The end result is that Form 1 can be rewritten as:

let val = (doSomething() >>=- doSomethingWhileIgnoring) >>=- process

Here, doSomethingWhileIgnoring(Int) -> Int? is a wrapper around doSomething that matches the required type for the right-hand-side of >>=-. The parentheses are there to emphasize that >>=- is defined as a left-associative operator.

As for Form 2, it can be rewritten as:

let val = doSomething() >>=- { ignore in
    doSomethingWhileIgnoring(ignore) >>= process
}

So these two forms have to be equivalent for our for-comprehensions to work. What we are dealing with here is a monadic value a:M<A> (the output of doSomething()), and two functions: f:A -> M<B> (doSomethingWhileIgnoring) and g:B -> M<C> (process). Our law then becomes that these two forms must be the same for all monads:

// Form 1                // Form 2
(a >>=- f) >>=- g   ==   a >>=- { b in f(b) >>= g }

That’s the third monadic law, for your sugary syntactic pleasure.

• • • • •

Whew! That was a lot of ground to cover, so let me give you a brief recap. A very common problem when dealing with optionals is dealing with multiple ones. We quickly end up with a bunch of nested if-let statements that look very bad—especially since most of the time, the recovery from any one of them failing is the same.

Other languages with optionals deal with this problem by introducing syntactic sugar; Scala calls it for-comprehensions, while Haskell calls it do-notation. They are similar, though not identical, and I endowed Magical Future Swift with the Scala version.

While investigating for-comprehensions I showed that they work for a specific class of types, which have one type parameter, and which follow three laws. These types are called monads, and they follow the following rules:

The three laws allow for-comprehensions to work the way we would intuitively expect them to:

The wonderful thing about for-comprehensions is that they don’t only apply to optionals—they are a general way of dealing with monads. They can apply to arrays, results, futures—any type that follows the rules above.

These rules might seem much ado about nothing, but if you look at a for-comprehension, it tends to look like a regular imperative program—but with some logic abstracted away. Beth’s birthday problem looked like regular assignments—except we didn’t have to worry about the nil case; everything was taken care of. Sam’s scheduling problem looked like it was doing regular assignments as well—abstracting away the fact that the code was iterating through multiple lists at the same time.

This means that each monad, in a way, defines its own imperative semantics when used in a for-comprehension—but in a way that still provides type safety and referential transparency. For-comprehensions therefore provide the best of both the imperative and the functional world.

• • • • •

Alright, I’m almost done. There’s one thing I still need to justify. I claimed, rather brashly, that monads had to do with the future of Swift. Of course, since I don’t work at Apple I can’t know for certain. But every language that has introduced optionals has had to introduce the associated syntactic sugar—dealing with them is too painful otherwise.

So because Scala, Haskell, and F-Sharp all sooner or later felt the need to introduce for-comprehensions, I feel fairly confident in predicting that they will make their way into Swift. What this would mean for the rank-and-file developer is some new syntax; this doesn’t require a deep understanding of monads—the whole point of for-comprehensions is that they work intuitively. I happen to believe, however, that taking the time to truly understand the semantics of your language can only help.

And if we can be a step ahead of the game by looking toward the future, so much the better.

• • • • •

Thanks for sticking with me through this series. There were seven total posts—beginning with error handling, continuing with understanding optionals, and ending with the three culmination posts. I hope I managed to convey why the monadic pattern is important, and to explain where the three laws come from.

“Important”, though, is not the same as “required knowledge”. The details of RAM fetching are important, but most developers can ignore them, and so it is with monads. At the end of the day, it is never the point of a construct to make life more difficult. As a dev, you’ll only have to worry about monads when trying to implement a type that can be used in a for-comprehension. For the rest, life will go on as usual.

But a little safer, and a little more functional. And isn’t that a good thing?

 
59
Kudos
 
59
Kudos

Now read this

Addendum: Deriving the Third Monad Law From Nested Comprehensions

The third monad law declares that the following identity must hold for a monad M, where a:M<A>, f: A -> M<B>, and g: B -> M<C>: (a >>=- f) >>=- g == a >>=- { b in f(x) >>=- g } To motivate... Continue →