(Incidentally, re-reading my posts above, I note that the text is very clunky in some places (for some reason, I often rush to compose and submit posts quickly rather than leisurely waiting till I’ve gotten them just right, with the expected results), so, if anything in my posts is confusing, please ask me to clarify)
Let’s toss another example of a monad (the term still at the moment undefined) into the mix: Maybe. A value of type Maybe T is either a value of type T, tagged with the label “Just”, or a special value called “Nothing”. Thus, the values of type Maybe Boolean are Just True, Just False, and Nothing; the values of type Maybe Integer are Nothing, Just 0, Just 1, Just -1, Just 2, Just -2, and so on. The reason I introduce this is because it’s another nice example of a monad (actually, every type constructor I’ve mentioned so far happens to give a monad, but for our pedagogical purposes, some are more natural than others).
So, the three monads I want to talk about right now are Maybe, List, and IO. What is it that these all have in common?
Answer*: For each of these type constructors M, we can naturally think of normal functions from the type A to the type M B as some special kind of “generalized functions” from the type A to the type B. For example, a function of type A -> Maybe B is like a function from A to B with the special ability to “abort” and return no value at all (i.e., to return Nothing). A function of type A -> List of Bs is like a multi-valued function from A to B, which can return more than one value at a time. And, of course, a function from A to IO B is like a special function from A to B with the ability to be nondeterministic, carry out side-effects, and all the usual craziness. Whenever we have a type constructor which can be viewed in this manner as giving a notion of “generalized function”, we call it a monad.
Alright, let’s make this informal definition more precise. What makes for a suitable notion of “generalized function”? Well, let’s say there are just two requirements:
- Just as normal functions include do-nothing “identity functions”, there should also be “identity generalized functions”.
- Just as normal functions can be composed together, with the output of one becoming the input to another, we should also have a way to compose together generalized functions.
Thus, our more precise definition of a monad is this:
A) A type constructor M. [For convenience’s sake, we shall say “M-function from A to B” to mean “normal function from A to M B”].
B) An M-function from A to A which “does nothing” (at all types A). [In Haskell, this function is named “return”]
C) A way to compose M-functions from B to C and from A to B into one from A to C, as though chaining them together (again, at all types). [Let’s denote the composition of g and f as g # f. This isn’t actual Haskell syntax, but whatever. (As we’ll see later, Haskell has special syntactic sugar that allows one to work with monads as though simply writing in a typical imperative programming language)]
Formally, the “do-nothing” condition in (B) is the condition that f, (return # f), and (f # return) are all the same. Furthermore, we require that composition be associative, which is to say, (h # g) # f should be the same as h # (g # f).
And there you have it. That is the entire definition of a monad. To recap, a monad is a type constructor M along with a way to see “function from A to M B” as “generalized function from A to B”, complete with an associative notion of composition which also has identity maps.
[In the next post, I will illustrate this definition by demonstrating how Maybe and List are monads.]
[*: Incidentally, another way to look at all this is that Maybe, List, and IO all act like “container classes”; that is, for each of these type constructors M, a value of type M T is like a container which, in some way, can be thought of as holding values of type T. With Maybe, the container can contain 0 or 1 many things; with List, the container can obviously contain a whole sequence of things; and with IO, the container will store things in recipe-form. If you think about it, this isn’t far removed from the perspective used above: for suitable notions of “container”, we can naturally think of “container-valued functions” as generalized functions, and, conversely, can think of generalized functions taking 0 arguments of input as containers. Thus, if you like, you can think “monad = suitable notion of container” instead of “monad = suitable notion of generalized function”.]