On this blog, I’ve featured a four part series on monads. I figure I can give at least one post to comonads. To start off, I will review CS monads at a high level (as opposed to categorical monads), and then begin to introduce the defining features of a CS comonad. Finally, I will give an example.
We are using Haskell syntax today, because it’s very lightweight and convenient for this.
Monads are types that encapsulate a value, and provide functions called bind and return. Return is the function that takes a value, puts it into the monad, and then returns the monad. If your monad is m, and your type is a, then the type of return is a -> m a. Bind is the function that allows for you to work on the data in the monad; it does a computation and returns a monadic value of the result. It’s type is m a -> (a -> m b) -> m b. Not essential for a monad mathematically, but included in a CS monad is the notion of fmap, or flatMap, with type signature (a -> b) -> f a -> f b, where f is a functor. This is called flatMap, as it flattens contexts out. In the original monad series, I gave the example in Scala of Option<Option<Int>> being equivalent to Option<Int>.
I have gone over the applications of this before, you can see for yourself that there are many. Beyond this, the obvious (1) question becomes “So how do we do it backwards?”.
You may have guessed that this is what a comonad is. So if a monad is all about encapsulating context-sensitive values, then what does that mean for comonads? Take a moment to think about that. If you chose not too, it’d be a real shame if I were to include a long filler sentence to make you wait anyway. Tension is exciting. Comonads are a way to take new data out of a source irrespective of context (2).
A comonad has three defining functions, extract, extend, and duplicate. Extract is analogous to return. If we have a comonad, which we usually denote as w, because it’s an upside down m, and that comonad is of a type a, then extract has type w a -> a. The symmetry is obvious. This is the easy to understand function; the other two are more in depth.
Extend has type signature (w a -> b) -> w a -> w b. For those of you that read Haskell type signatures well, the behavior of this function is easily determined. Extend takes a function that extracts b‘s out of w a‘s, a w a, and then returns a w b. As comonads usually store a collection of values, this is a map-like operation. This is symmetric to Bind.
I’ve drawn a diagram. The first diagram is for a monad. It represents Bind. Given an m a from somewhere unspecified (I drew the arrow, as this m a is usually a function call), and a function that maps from a to m b, you map your m a to your m b.In the second diagram, we have extend. I am using M for a comonad in this diagram because the symbols are arbitrary for the demonstration, and paint is really bad for editing text. Extend takes a function that takes a b out of our comonadic a, a function that takes a c out of our comonadic b, and returns a function for taking a c out of a comonadic a. This parallels Bind’s ability to chain together computations, as you can plug an extract into another extract just like you can plug a bind into another bind.
The last function of note is duplicate. This is analogous (I keep saying analogous, though the mathematical term is dual) to fmap. Duplicate nests contexts, and has type signature w a -> w (w a). The use of this is that in a functional program, a container has a focus. This focus is one element of the container; it’s what we call a context. This looks like it complicates things, and it does, but hear me out. What duplicate returns is a container of containers that each has a different focus. This allows for functions like fmap to work on all of the elements at once.
Now, I can’t just dump this on you without giving an example. Believe it or not, streams are comonads. Intuitively, this makes sense, as streams operate on codata. Before I go further, let’s define a stream. In Haskell, data stream s = Cons s (Stream s). This is pretty much a list without a base case. Extract is to return the current element, duplicate, on a Cons of s and stream, is to return Cons (Cons s stream) (duplicate stream). Extending a function f on a stream is to flatMap that function applied to the duplicate function.
(1) This had never occurred to me, even though I had been doing it for a while. Streams are comonads, as I will introduce later.
(2) “Irrespective of context” is a confusing statement, so I’ll give some examples. Representing time sensitive data asynchronously is a use of comonads. Representing read only memory in a mutable environment is a use of comonads.