Thursday, February 14, 2013

Haskell programmers popularized the use of monads to structure imperative computations, complete with syntactic sugar in the form of do notation. However, category theorists predict that there is also a completely symmetric concept called a "comonad", which tickles our fancy. After all, if monads are so intriguing, then perhaps comonads are equally intriguing, and who doesn't like twice the intrigue?

However, there's nothing intriguing about comonads. In fact, comonads are so pervasive in modern programming that they need no introduction. I hope to show that you very likely already employ a comonadic programming style, although you probably prefer to call it "object-oriented programming".

In this post I will build the metaphor of comonads as objects and also introduce syntactic sugar for comonads that strongly motivates this object-oriented interpretation.

The Builder Pattern

Let's try to reimplement the objected-oriented builder design pattern in Haskell. For example, let's say that we have a Config value we want to build that just stores a list of program Options:

We want to encapsulate the Config and prevent the user from disassembling it, so we don't export the MakeConfig constructor to prevent the user from pattern matching on the constructor. Instead, we export a function in place of the constructor so that the user can still create values of type Config:

... but I wouldn't be able to use both profile and goFaster on the same builder.

In object oriented programming you don't have this problem, because you usually structure your builder to append options instead of setting them so that you can feed in additional options later. Then you extract the final configuration when you are done:

However, Haskell has no implicit state, so we cannot mutate the original builder like the objected oriented version does. Even if we could, it's not clear where we would store additional options since our builder is a function, not a record with fields.

Instead, we must cheat and modify our original functions to return a new builder that "precompiles in" the given options:

We transformed our option setters into option appenders that leave our builder open just like the object-oriented version. Additionally, we need some sort of extract function that completes the process and returns the final configuration. That's easy since we can flush the builder by supplying it with an empty option list:

However, going back and upgrading all possible option setters into option appenders is a painstaking and laborious process, especially if we already defined a lot of them. Haskell is a functional language, though, so could we just write a function to automate this upgrade process?

In other words, we already wrote some option setter that closes the builder when done:

setter :: ([Option] -> Config) -> Config

... but we want to extend that setter to behave like an appender that keeps the builder open:

extend setter :: ([Option] -> Config) -> ([Option] -> Config)

This means that our extend function must have the following overall type:

You might expect that this isn't possible. After all, option setters seem to irreversibly use up the builder. However, we can actually cheat by intercepting the setter before applying it to the builder to reserve space for future options:

However, all we've proven is that extend works for our goFaster function. How could we prove that it always works in every possible case? We might try to write some tests, perhaps by defining some example option setters, upgrading them to appenders, and simulating a few build processes, but these tests would only prove that extend works in those few cases that we tested. Could we instead prove that extend does the right thing in every conceivable scenario? Is that even possible?

Fortunately, Haskell's enforced purity unlocks a more powerful and more general alternative to testing: equational reasoning. We simply define the expected behavior of our extend function in terms of equations and prove that extend satisfies those equations. But what might that expected behavior be?

Well, we expect that if we extend a setter to become an appender, apply it to a builder, and then immediately extract the result, it should behave as if we had directly used the setter. The equation for that would be:

extract (extend setter builder) = setter builder

In other words, there's no point in extending a setter if we plan to extract the result immediately afterwards.

We might also expect that if we extend the empty option setter (i.e. extract), the resulting appender should not change the builder since extract doesn't supply any options:

Exercise: Prove that:

extend extract builder = builder

There's one last property that's really subtle, but important. If we choose not to extend the second of two setters, we can always go back and extend the second setter later as long as we haven't applied them to the builder yet:

Interestingly, these three equations suffice to completely describe what it means to convert an option setter into an appender. We can formulate any other property in terms of those three properties. This means that we no longer need to test our function at all because we've proven it correct in all possible circumstances.

For example, we might suppose that if we choose not to extend the third of three setters, we can always go back and extend the third setter later on as long as we haven't applied them to the builder yet:

In fact, the proof for the two-setter case generalizes to any number of stages. In other words, if we choose not to extend the last setter in an arbitrarily long chain, we can always go back and extend the last setter later if we haven't applied them to the builder yet.

You might be surprised to learn that we've inadvertently defined our first comonad in our pursuit of simulating object oriented programming, but it might not be clear what exactly a comonad is or why those three equations are the only equations we need to prove.

The Iterator Pattern

Now let's imagine that we're writing a new terminal shell, complete with command line history. We could model history as an infinite iterator that stores events in reverse chronological order:

data Iterator a = a :< (Iterator a)
infixr 5 :<

The history would start off empty, which we model as an infinite stream of empty commands:

Duplication with minor changes is a code smell. I would like these two functions to remain consistent such that next3' always shifts to the value that next3 points to, but when I duplicate the code I must manually enforce that consistency between them. The "Don't Repeat Yourself" principle says you should not duplicate code precisely to avoid this problem.

Moreover, even the initial duplication could introduce mistakes, such as accidentally deleting one step while modifying the code:

next3' :: Iterator a -> Iterator a
next3' iterator = next' (next' iterator))
-- Oops! I deleted the next and my boss
-- distracted me in the middle of the process
-- so I forgot to replace it with next'

We'd prefer to automate the conversion process both to avoid introducing errors and to enforce consistency between the two functions. In other words, I need a function that takes some sort of retrieval:

Boy, that's a lot of effort to prove that extend upgrades one retrieval correctly. I'd prefer to pick my proofs wisely so that I can prove that extend upgrades all retrievals correctly. But what proofs suffice to nail down what it means to convert a retrieval into a shift?

Well, I expect that if I upgrade a retrieval into a shift, apply the shift, then extract the value under the cursor, I should get the same value as if I had just used the retrieval directly:

Exercise: Prove that:

extract (extend retrieval iterator) = retrieval iterator

This proof is simple and requires no fancy tricks.

Since extract points to the current location, I expect the equivalent shift to not move the iterator at all:

Again, there's one last non-trivial property. If I choose not to extend the second of two retrievals, I can still extend the second retrieval later if I haven't applied both of them to an iterator yet:

Notice how we arrived at the exact same three equations, despite solving two completely unrelated problems. Something very deep seems to lie just beneath the surface.

The Command Pattern

Now imagine that we are trying to build a small DSL for manipulating a thermostat. The thermostat keeps an internal floating point temperature in Kelvin, which it presents to the user as a string representation in Celsius. The thermostat comes with a simple API:

Increment the temperature by one degree

Decrement the temperature by one degree

This is the classic text-book example of objected-oriented design where we have some object that maintains state and methods on that object. Unfortunately, we're gluttons for punishment and choose to implement this solution in Haskell, one of the most decidedly anti-object-oriented languages in existence.

We choose to model our thermostat as a pair of values:

The internal representation of temperature, in Kelvin

A function that reads out the temperature into various representations

However, there's a potential source of bugs in this approach. When a user selects a given preview, nothing requires us to apply an adjustment that matches the preview. We can only enforce this by being sufficiently diligent programmers. Wouldn't it be nice, though, if we could eliminate an entire class of bugs by automatically converting the preview into the correct corresponding adjustment?

In other words, we have a preview function:

preview :: (Kelvin, Kelvin -> a) -> b

... and we want to extend it to behave like the corresponding adjustment function:

Which one is the "right" version? Well, we can decide which implementation is the correct one by specifying the desired behavior in the form of equations, and then seeing which implementation satisfies the equations.

We first expect that if you extract the result of an adjustment, it should match the corresponding preview.

Exercise: Prove that:

extract (extend preview thermostat) = preview thermostat

Both extend up and up' satisfy this criterion, so we can't yet say which one is correct.

We also expect that if you extend a preview of the current state (i.e. extract) to become an adjustment, then this adjustment should do nothing.

Exercise: Prove that:

extend extract thermostat = thermostat

This criterion doesn't apply to any previews other than extract, so we can't use it to select between extend up and up'.

This leaves us with our final equation: If you combine an adjustment and preview, you can extend the pair of them into an adjustment, which should be identical to extending the preview alone.

This proves to be the acid test that eliminates our original implementation of up'.

Challenge Exercise: Prove that our original implementation of up' does not play nice with extend. In other words, find a counter-example that proves the following equation false:

extend (\t' -> p (up' t')) t = extend p (up' t)

Challenge Exercise: What bug does the above counter-example predict may happen when you mix extend with up'?

Comonads

At this point, we've tackled three separate object-oriented problems and in all three of them we define an extract and extend function that seem very similar across all three cases. We already know that these two functions always seem to obey the same set of equations, despite having very different types in each case.

However, we can squint a little bit and realize that the types are not as different as they initially seem. Define:

The combination of w, extract, and extend is a comonad. The equations we've been proving are known as the "comonad laws".

Comonad laws

Why do we always seem to come across the same set of equations each time? Well, we can get a clue if we define a derived operator in terms of extend:

(f =>= g) w = g (extend f w)

This operator has the nice property that extract is both its left and right identity:

extract =>= f = f
f =>= extract = f

Also, this operator is associative:

(f =>= g) =>= h = f =>= (g =>= h)

In other words, comonads form a category, specifically a "CoKleisli" category, where (=>=) is the associative composition operator for the category and extract is the identity of this composition. The comonad laws are nothing more than the category laws in disguise.

If you take the last three equations and substitute in the definition of (=>=), you will derive the same three comonad laws we have repeatedly proven.

Object-oriented programming

Every time we try to implement object-oriented programming in Haskell we gravitate, inexorably, to modeling objects as comonads. This suggests that object-oriented programming and comonadic programming are one and the same. Haskell programmers have struggled with comonads because we so militantly rejected object-oriented programming.

This means we should approach object oriented programming with humility instead of disdain and borrow object-oriented insight to devise a suitable syntactic sugar for programming with comonads.

Syntactic sugar for comonads

The object-oriented intuition for comonads suggests the syntactic sugar for comonads, which I call method notation.

Notice how it looks like we are declaring an anonymous method and immediately invoking it on the defaultConfig object. Also, we only use setters and yet the method does "the right thing" and does not extend the last setter, as if we were returning the last line directly.

Here we are declaring next3 as if it were part of a class definition. method notation implicitly makes it a function of the corresponding object and brings this into scope, which always implicitly refers to the current state of the object.

If we want to refer to previous states, we just bring the intermediate steps into scope using the "prompt" notation:

We don't have to carefully keep track of whether or not we should use them as accessors or "mutators". method notation automatically gets that correct for us.

Notice the duality with do notation and monads. With monads, unwrapped variables are scarce since we can irreversibly convert them to wrapped values, so do notation keeps them in scope as long as possible. With comonads, wrapped variables are scarce since we can irreversibly convert them to unwrapped values, so method notation keeps them in scope for as long as possible.

Our thermostat example looks just like the classic object-oriented example using method notation, except we don't need to say return at the end as method notation always implicitly returns the value of the last line:

Any function we apply to an invoked method can be converted to a post-fix application:

toString (w # method
this # up
this # up
this)
= (w # method
this # up
this # up
this) # toString

Observe how suggestive the notation is. When we use post-fix syntax, we can just remove the parentheses and it is still correct:

(w # method
this # up
this # up
this) # toString
= w # method
this # up
this # up
this # toString

The post-fix application style plays incredibly nicely with method syntax, even though I never designed it for that purpose. This suggests that the object-oriented tendency towards post-fix function application style falls out naturally from the comonadic style and is not just a quirk of object-oriented tradition.

Comonad laws

The acid test of syntactic sugar is that the notation promotes an intuition for the comonad laws. Let's write the comonad laws using method notation:

... and like do notation, we can flatten both forms to a single normal form:

= w # method
this # f
this # g
this # h

The notation suggests quite naturally that if you call a method on this, it's equivalent to inlining the method directly. Conversely, you can arbitrarily factor out any group of steps into their own method.

Comonad transformers

do notation has the nice property that it promotes the correct intuition for the monad transformer laws, where the lift distributes over the do block:

Conclusion

Object-oriented programming is the long-lost comonadic programming that Haskell programmers have been searching for. Comonads are "object-oriented programming done right".

Monads and do notation transformed the face of Haskell by turning it into the finest imperative programming language. I believe that comonads and method notation may similarly transform Haskell into the finest object-oriented language as well.