Another view on a monad
Wikipedia describes a monad in some Haskell syntax. Oh well. But actually, one doesn’t need to know Haskell to understand what a monad is and how it can be used.
Basics
Suppose you have a Future<String> but that string is actually a decimal integer so, you want to parse it. There are two ways:
- Imperative: wait for the
Futureto resolve, then parse into ani32. - Monadic: apply the parser “right now” getting a
Future<i32>as a result.
Both ways can also be used when instead of parsing you need something that is itself asynchronous, i.e. returns Future<i32> and not i32; in either case you’ll get a Future<i32> as the result.
While imperative code is generally easier to follow monadic version is often leaner on resources. That’s why the modern async/await syntax blurs the distinction a bit: it looks like 1 but works more like 2, evidenced by the fact the result (of an async block or function) is a Future and not the computed value itself.
Generalization
So far I was only talking about Future specifically. But the approach can be generalized and, that generalization is what a monad is.
In more concrete terms a monad M is a generic type M<T> which allows applying functions T -> U and T -> M<U> to itself. As there may not be a T inside at the time the result can’t be plain U so, it is M<U> in either case.
There are many examples of monads. The textbook one is Option (aka Maybe). But while tremendously useful on its own its monadic nature is shadowed somewhat: sure one can do map or and_then on it but, one can do if let Some(x) { y += x; } just as well and, that’s not at all monadic. Except in Haskell where the very notion of monad originated, one can’t: while one can pattern-match one has to return something regardless of the match and, there is no imperative stuff like += in Haskell whatsoever so, just going the monadic way may very well be easier.
Composition
Another way to look at monads is in the parlance of monadic composition.
Suppose you have two functions, f(A) -> B and g(B) -> C. You can compose them into a new function f_then_g(A) -> C. However, if what you have is f(A) -> M<B> and g(B) -> M<C> regular composition doesn’t work as types don’t match. But monadic composition does!¹ The result is a function f_then_g(A) -> M<C> which applies g to f’s result effectively even though it can’t do that directly.
Outside of Haskell though composition often isn’t spelled out. But it is still there. Consider:
fn download(url: &str) -> Vec<u8> {
let data = fetch(url);
let data = decrypt(data);
let data = decompress(data);
data
}
It’s actually just a composition of 3 functions: fetch, decrypt, and decompress. But in many cases that’s not possible as-is. E.g. fetch is likely asynchronous. So the code would be more like this:
async fn download(url: &str) -> Vec<u8> {
let data = fetch(url).await;
let data = decrypt(data).await; // maybe it’s hardware accelerated?
let data = decompress(data).await;
data
}
And this is actually a monadic composition of the 3 functions. Indeed fetch being async doesn’t return a Vec<u8> anymore but a Future<Vec<u8>> instead while decrypt still takes a Vec<u8> as an argument, yet they are essentially composed together.
In the last example await is the key: it’s what facilitates the composition, and it is also specific to Future. But Future isn’t the only monad out there. The first example was overly simplistic: actual functions would return a Result and one’d need to handle that as well, like:
fn download(url: &str) -> io::Result<Vec<u8>> {
let data = fetch(url)?;
let data = decrypt(data)?;
let data = decompress(data)?;
Ok(data)
}
And this is again, a monadic composition of the 3 functions. The monad is io::Result this time. It has its own special syntax (shared for Result and Option), so short because it tends to be everywhere in Rust.
¹ Provided that M is a monad of course.
How this works
There is no direct support of composition in the definition of monad. But it is actually pretty easy: if you have f(A) -> M<B> and g(B) -> M<C> you can just call f and apply g to the result.
It actually works the other way as well: if you have an x: M<A> and need to apply f(A) -> M<B> to it using composition, you can wrap x as h(()) -> M<A> (returning x unconditionally), monadically compose h with f, and call the resulting (()) -> M<B> function to obtain the M<B>.
Conclusion
While directly working with monads may be a hassle they are really useful as building blocks, allowing to unify such distinct concepts as error handling, asynchronous programming, and more.