Monads and Comonads

Monads are probably the most dreaded concept by newcomers to Functional Programming. They have a lesser known counterpart, Comonads, which happen to have a very special relationship with Monads. This page does not aim to be a tutorial on Monads and Comonads; rather, we will try to build an intuition of what they are, by reading the operations included in these type classes, and the types involved in them.

Monads

In Category Theory, Monads are defined as Monoids in the Category of Endofunctors. This definition, although correct, is mostly useless in software development. What we need to consider is what are the requirements for something to behave as a Monad.

First thing we need to point out is that Monads cannot be represented as an abstraction in Swift; the Monad type class is an abstraction that works at a Higher Kind level. That is, it must be conformed by a type F<A>, for all A. Unfortunately, this is not possible to be expressed in Swift. Bow provides an emulation of Higher Kinded Types that will let us describe this abstraction.

Then, in programming, the Monad type class requires the implementation of two functions:

In ocasions, instead of flatMap, you could implement:

flatMap and flatten can be implemented in terms of each other; therefore, in order to have an implementation of a Monad, you must provide implementations of pure, and flatMap or flatten. In Bow, you will always be required to implement pure and flatMap.

Let’s look at the types of each required function. pure is a function (A) -> F<A>. That is, given any value, the pure function can lift it to the context of the Monad. In this sense, we can say that monadic operations “introduce context”.

flatMap is a function (F<A>, (A) -> F<B>) -> F<B>. It has two arguments: F<A>, which we can read as “a value in the context of the Monad”; and a function (A) -> F<B>, which we can read as “a function to produce a new value in the context of the Monad”. Looking at the return type, F<B>, the only way we can obtain it is by running the function provided as an argument, but to do so, we need a value of type A. We can somehow obtain an A from the first argument, given that it exists in the context of the Monad. Therefore, intuitively, the flatMap operation lets us perform two effects sequentially, when the second (F<B>) depends on the first (F<A>). In this way, we can say that Monads let us “chain dependent effects sequentially”.

In summary, from this intuition we can say that Monads introduce context in the operations they are involved, and let us chain effects sequentially.

Example

One example of a Monad that is pervasively used throughout the library is State. State<S, A> represents a function (S) -> (S, A); that is, a function that receives a value of the state model, and produces a tuple with a modification of the provided state, and an output value of type A. State is used to represent computations that depend on a certain state, without having to thread it explicitly through all operations.

struct State<S, A> {
    let run: (S) -> (S, A)
}

It’s instance of the Monad type class (its implementation) is pretty straigthforward. Let’s begin with pure: given any value of type A, we can always provide a State<S, A>, that does not modify the passed state:

extension State {
    static func pure(_ a: A) -> State<S, A> {
        State { s in (s, a) }
    }
}

As for flatMap, we mentioned that, from our intuition, we are sequencing two operations, where the second one depends on the result of the first. That means we should run the first State, obtain the modified state and the output, and feed it to the second:

extension State {
    func flatMap<B>(_ f: @escaping (A) -> State<S, B>) -> State<S, B> {
        State<S, B> { s in
            let (newS, a) = self.run(s)
            return f(a).run(newS)
        }
    }
}

Comonads

Similarly, Comonads could be defined as Comonoids in the Category of Endofunctors, which is an equally useless definition in software development. Comonads the dual structure of Monads, obtained by reversing the arrows in Category Theory.

As Monads, Comonads work at the Higher Kind level, and are only possible to be represented using the emulation provided by Bow. They require the implementation of the following requirements:

Sometimes, instead of coflatMap, you could implement:

coflatMap and duplicate can be implemented in terms of one another; thus, you need to implement extract, and coflatMap or duplicate, to have an implementation of a Comonad. In Bow, you will always have to implement extract and coflatMap.

By now, you may have already noticed some symmetry between Monads and Comonads, but let’s look at the types of the functions in order to build some sort of intuition behind them.

extract is a function (F<A>) -> A, which, if you pay attention, is just the opposite of pure. That is, given a value in the context of the Comonad, we are able to extract that value out of the context. This tells us the Comonad represents some kind of space, but it is focused on a specific point of such space, which we can always obtain.

coflatMap is a function (F<A>, (F<A>) -> B) -> F<B>. That is, we need to return an F<B>, which as we have mentioned above, can be seen as a space of values of type B. The only way we can obtain values of type B is by the function (F<A>) -> B, but an invocation of this function gives us a single point B in our space. This suggests we will need to invoke this function potentially multiple times to build the space of F<B>, and each time, it consumes the context provided by F<A>. Therefore, coflatMap lets us perform an operation (F<A>) -> B that consumes the context of F<A>, in all posible foci its space of values, to produce a new space of values.

In summary, Comonads let us perform operations that are context-dependent, and extract their focused results.

Example

Store is also used extensively in Bow Arch. Store<S, A> wraps two things: a value of type S, known as the state, and a function of type (S) -> A, known as render.

struct Store<S, A> {
    let state: S
    let render: (S) -> A
}

From the intuition we built before, we said a Comonad represents a space of values. Such space is represented by the render function in the Store. It models all possible A values that could be potentially rendered by this Store. Also, we mentioned that Comonads are somehow focused on a specific point of such space; in Store, that focus is the state.

How does its Comonoad instance look like? The extract function should be easy to implement: just apply the render function to the current state:

extension Store {
    func extract() -> A {
        self.render(self.state)
    }
}

The implementation for coflatMap may be a bit more cumbersome to understand. We need to provide a Store<S, B>. The state property for such Store is the same state of the receiver Store, as we have no other way of getting such value.

Regarding the render function, we need a function (S) -> B. The only thing we have to obtain a B is the provided function (Store<S, A>) -> B. Therefore, we can construct a new Store with the render function of Store<S, A>, and pass it to the provided function.

extension Store {
    func coflatMap<B>(_ f: @escaping (Store<S, A>) -> B) -> Store<S, B> {
        Store<S, B>(
            state: self.state,
            render: { s in
                f(Store(state: s, render: self.render))
            })
    }
}

As you can see, the function (Store<S, A>) -> B is providing us a specific point of the new space in Store<S, B>, and when we do a coflatMap, we are potentially exploring all possible contexts (with the new render function) to build the space of Store<S, B>.

Stores are focused on a specific state, but also provide methods to change that focus, to render a different point of the space of options they model.