Just like everybody else who starts to look at monads, I found it was like coming to the face of a sheer cliff. Let me qualify that: just like every other programmer who is not a mathematician (and that’s most of us). I am looking at monads in the context of clojure, so code snippets will generally be written in clojure-like syntax.
I have found these resources the most helpful:
- Adam Smyczek’s Introduction to Monads on YouTube
- Santosh Rajan’s blog Functional JavaScript
- Jim Duey’s discussion of monads on his blog about Clojure
Adam’s talk eschews category theory, and mathematics in general, concentrating on how to define and use monads in code. This is an essential approach for the rest of us. Unfortunately, all of the terms used in discussing monads hark back to the mathematics, and that, I believe, makes the practical use of monads more confusing than it need be. This point was brought home to me strongly when, after observing what some of the others were saying, I watched the first part of a lecture by this extraordinary woman.
Warning: take everything I say with a large grain of salt. I am writing this to help me to sort it out in my own mind, and there are no guarantees that any of it is correct.
In all of the discussions I have seen, it is stressed that monads are defined in terms of the behaviour of two functions.
wrap
Although it is not usually mentioned first, I will start with the function that I will call wrap. Nobody else calls it that, but that’s what it does. It goes by a number of aliases:
- result, m_result, mResult, etc.
- return, m_return, etc.
- lift, m_lift, etc.
- the monadic function (? Sometimes. However, the 2nd argument to rewrap —see below— is generally called monadic function, so it may be more accurate to describe wrap as the base function on which all other monadic functions of a given monad are built.)
f: T -> [T]
That is, for the Array Monad, wrap takes a value of some type T, and wraps it in an array. Both the type of value and the mode of wrapping are specific to each defined monad, but once defined, they are fixed for that monad.
The resulting, wrapped, value returned from wrap is known as a monadic value, and is often represented in code as mv. In this discussion, I’ll call it wrapt, for obvious reasons. I’ll call the argument to wrap the basic value, or bv.
;; Takes a basic value; returns a wrapt value (defn mymonad-wrap [bv] (let [wrapt (do-the-wrapping-of bv)] wrapt))
You can see why monadic values are frequently called containers.
(unwrap)
unwrap is under wraps, so to speak, because it is not part of the public face of monads. It is NOT one of the two functions which define the behaviour of a monad. It is, however, essential to the functioning of monads, and some kind of unwrap functionality must be available to the other function in the definition of monad: rewrap.
unwrap is the inverse of wrap, not surprisingly. In terms of Santosh’s Array monad, it’s signature might look like this.
f: T <- [T]
That is, unwrap takes a wrapt (monadic value), which is a wrapped basic value and returns the basic value.
rewrap
This the function that is generally called bind, m_bind, etc., although in Santosh’s examples, this function takes the name of the monad; for example, arrayMonad. The signature that Santosh gives for this function in the Array Monad is
M: [T] -> [T]
That is, it transforms a wrapt value to another wrapt value. In the case of the Array monad, the basic value is wrapped in an array.
rewrap looks like this.
(defn mymonad-rewrap [wrapt, mf] ;; call monadic function given as 2nd arg, on the ;; basic value extracted from the wrapt value given ;; in the 1st argument. Return the new wrapt value. (let [bv (do-unwrap-fn-on wrapt) new-wrapt (mf bv)] new-wrapt))
So, what rewrap does is
- unwrap its monadic value argument to get the basic value
- call its monadic function argument with the unwrapped basic value to…
- modify the basic value
- wrap the modified basic value and return a new wrapt value
The monadic function argument, mf, deserves a closer look. mf operates on a basic value to produce a new wrapt value. It is, in fact, a composition of functions. It composes wrap and some operation that modifies the basic value. So,
mf ⇒ (f′ ⋅ wrap)
where f′ is a function that modifies the basic value. In that scheme, wrap itself can be described as
wrap′ ⇒ (identity ⋅ wrap)
That given, we can now describe rewrap as
(defn rewrap [wrapt, (f′ ⋅ wrap)]
(let [bv (unwrap wrapt) new-wrapt (wrap (f′ bv))] new-wrapt))
or, equivalently,
(defn rewrap [wrapt, (f′ ⋅ wrap)]
(wrap (f′ (unwrap wrapt))))
The 3 R’s
Monads must obey three rules. These rules I have taken from Jim Duey’s post, with the appropriate translation to the “wrap” terminology I’m using here.
Rule 1
(rewrap (wrap x) f) ≡ (f x)
Alternatively, given our rewriting of rewrap, above, but using f rather than (f′ ⋅ wrap);
(f (unwrap (wrap x)) ≡ (f x)
⇒ (f x) ≡ (f x)
Rule 2
(rewrap wrapt wrap) ≡ wrapt
⇒ (wrap (unwrap wrapt)) ≡ wrapt
⇒ wrapt ≡ wrapt
Rule 3
(rewrap (rewrap wrapt f) g) ≡
(rewrap wrapt (fn [x] (rewrap (f x) g)))
LHS ⇒ (rewrap (f (unwrap wrapt)) g)
⇒ (g (unwrap (f (unwrap wrapt))))
⇒ (g (unwrap (f x))) [1]
⇒ (g (unwrap (wrap (f′ x))))
⇒ (g (f′ x))
RHS ⇒ (rewrap wrapt (fn [x] (g (unwrap (f x)))))
⇒ ((fn [x] (g (unwrap (f x)))) (unwrap wrapt))
⇒ ((fn [x] (g (unwrap (f x)))) x) [2]
Everything looks pretty straightforward, except for the correspondence between [1] and [2] in Rule 3. Something seems odd about it, even though the results will be the same.
That is looks so complicated