Monads Aren't That Hard
You should know what sets and types are, what a function is, and the consequences of composing functions.
In a programming language, you have types that are given to you and types that you can construct with your own definitions. Some examples of these are Int
, Bool
, and function types Int -> Int -> Int
. You may also may have types that generalize over types, an example is the type Optional<T>
where T
refers to any type. Notice that there is a difference between the types Optional<Int>
, Optional<Bool>
, and Optional<T>
.
We can construct the set $T$ of all types in some programming languages ($T$ may be infinite but it doesn’t matter). Since \(\texttt{Optional<T>} \in T \) , we can construct the subset
to be the set of all optional types. It is again worth noting that
\(\texttt{Optional<T>}\not\in O\)
since Optional<T>
must have T
refer to a specific type. Now we can define a function
which maps each type $t\in T$ to its optional type $o(t)=\texttt{Optional<$t$>}\in O$. It’s also clear that we can refer to $o$ in place of Optional
, and $o(t)$ in place of $\texttt{Optional<$t$>}$. This may take a sec to sink in. This structure can be generalized to any sort of generic type, consider the function
that corresponds to a generic type, in the same way $o$ corresponds to Optional
. We can now impose additional structure on $m$ to refine the set of types that can correspond to $m$ which are of interest to us. These requirements are of two parts, existence of other functions that interact with $m$, and algebreic laws that should hold for the interplay of these functions. The restrictions are as follows: For all types $t\in T$, there must the function
that assigns every value $a\in t\in T$ of type $t$ to the value $i(a)\in m(t)\in U$ (remember $t$ and $m(t)$ are types/sets). You can think of $i$ mapping a value of $t$ to a generic value of type $m(t)$. There must also exist the function
which is sort of odd to think about, but basically it maps values that are of a generic type $m$ of generic type $m$ to values of generic type $m$. Alternately, $s$ unwraps or squishes a double application $m(m(t))$ to a type $t$ to just a single application $m(t)$. An example for Optional
would be
which has the obvious implementation. Lastly, there needs to be the map
which is a function that takes some function $g : t \to u$ and returns a function $f(g) : m(t) \to m(u)$. For Optional
, this would be
which also has the obvious implementation. These functions give us enough to define the convenient binary operation
where the function $h : t \to m(u)$ maps some value $a\in t$ to the value $h(a)\in m(t)$.
Finally, the algebraic rules for these functions are the following equalities, for all $t,u\in T$ and all $a \in t$ or $a : t$, $b \in m(t)$ or $b : m(t)$, and the functions $g : t\to u$, and $h,k : t \to m(u)$,
where the left equalities is use the more concise $\succ$ operation. This concludes the definitions required for the definition of a monad. If there is a generic type M<T>
that coresponds to $m$ in the way we have outlined, there exists the functions above that operate on $m$ and fit the type structure, and those functions satisfy the three laws, then we say M
behaves like a monad. That’s all it is really, a generic type, with certain functions, satisfying some weird laws.
Above is insufficient to understand the benefits and patterns that monads provide. Usually the laws are disregarded as the type structure is usually enough to constrain the possible implementations. It isn’t obvious, but monadic behavior captures a very important sense in computing: composition of computational effects, which when understood and used correctly, makes you feel like a poet.
Additional Notes
Most of the time, it makes the most sense to define just the two functions
as the existence of the other $s$ and $f$ functions are implied by the two above.