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:
pure
orreturn
:(A) -> F<A>
flatMap
,bind
or>>=
:(F<A>, (A) -> F<B>) -> F<B>
In ocasions, instead of flatMap
, you could implement:
flatten
:(F<F<A>>) -> F<A>
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:
extract
:(F<A>) -> A
coflatMap
orextend
:(F<A>, (F<A>) -> B) -> F<B>
Sometimes, instead of coflatMap
, you could implement:
duplicate
:(F<A>) -> F<F<A>>
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.