People who are trying out Haskell quickly run into the concept of the monad, typically around the time they want their programs to actually do something. Two monadic types that new Haskell programmers will encounter are
Maybe. Having an understanding of how to use these is pretty important to writing useful code in Haskell. However, I don’t think these are necessarily great examples for introducing the concept.
Why async is a monad
If we have a long-running process, we can block the thread and wait for it to return us a result, but that’s far from ideal. Instead we return a “promise”, a structure that we can provide with a callback (using the
then method on the promise) to manipulate the result of our asynchronous processing.
The types look something like this:
Promise<T1>.then(T1 => Promise<T2>): Promise<T2>
We call our async function, which gives us a
Promise object wrapping a value of type
then method accepts a function from type
T1 to type
T2, and returns us a
Promise wrapping a value of type
Let’s look at the type of monad
>>= in Haskell:
m a -> (a -> m b) -> m b
bind accepts a Monad instance wrapping a value of type
a, and a function from type
a to type
m b (our Monad type wrapping type
b). It then returns us the
m b that the function returns.
The types of
then are very similar!
Promise is (approximately) a monad.
Why it matters
The fact that we can’t just “get” the result of the Promise and instead need to provide a callback is a core part of my understanding of the use of monads in Haskell — they are used in Haskell to represent the result of a non-deterministic effect. You can’t (or shouldn’t) just get the value out of a monad, in the same way that you can’t just get “the” value out of an array — an array has more than one value, so how can you get “the” value?
The use of monads to represent non-deterministic effects is a software engineering choice. The “monad” itself is just the structure of computation where you have a wrapped thing
M a, to which you apply a “callback”
a -> M b, to receive a monadic result
M b. There are also some laws which a well-behaved monad needs to follow. The point of this is that even though that
b might error out (option), or might come about later (promise), or might be multiple values (list), the
M b can be treated as a plain-old function result and the type checker is able to make stronger guarantees. Pretty handy when your language is based around pure functions.
Side note: async/await is pretty much do-notation for Promises. You write imperative-looking code and it gets magically turned into asynchronous callbacks for you.
Why I think it’s a better first monad
Maybe are both essential tools in any practical Haskell code. However, they each have their flaws as introductory monads.
IO is conceptually difficult, and the idea of codifying real-world interaction in the type system is likely to be unfamiliar to many programmers. In contrast, promises and async/await (or something similar) are used in a variety of popular programming langauges. Programmers are likely already somewhat familiar with how it works, and the idea of interacting with this effect through a monadic interface. This means one less thing to learn.
Maybe on the other hand is much simpler. Monad tutorials will often provide a simple implementation of it. However, I think it may be too simple. With async, it can be understood that you can’t just “get” the value, as it may not exist yet. In contrast, it may seem like interacting with
bind is unnecessary. Sometimes it is.
List is sometimes mentioned as well. It has a defined monad instance, and there are reasons why it is defined that way. I reckon it might be more likely to confuse, and raise questions like, “yeah, but WHY is it defined like that?”
That was my experience anyway. Worth looking at, but not first.