Suppose we have a task: a function takes an integer and divides it by 2. If the result is odd, the function returns no value. If the result is even, it divides it by 2 once more and then multiplies it by 10 if the new result is even, and by 5 if it is odd. Let's first write such a function in TypeScript.
function processNumber(value: number): number | undefined {
const halved = Math.floor(value / 2);
if (halved % 2 !== 0) {
return undefined;
}
const halvedAgain = halved / 2;
return halvedAgain % 2 === 0
? halvedAgain * 10
: halvedAgain * 5;
}Now let's rewrite this code in Haskell head-on. I'll say right away: this is not how things are done in Haskell.
processNumber :: Int -> Maybe Int
processNumber x =
let halved = x `div` 2
in case even halved of
False -> Nothing
True ->
let halvedAgain = halved `div` 2
in case even halvedAgain of
True -> Just (halvedAgain * 10)
False -> Just (halvedAgain * 5)What clearly looks bad in the code above is the nesting. TypeScript has none, but to understand what the function returns you have to keep a careful eye on every return. Nesting, on the other hand, shows the direction of the computation unambiguously. In an imperative language it is clear anyway: code runs top to bottom. Let's rewrite the code, splitting the computation into two functions:
halveEven :: Int -> Maybe Int
halveEven x =
let halved = x `div` 2
in case even halved of
False -> Nothing
True -> Just halved
halveAndScale :: Int -> Maybe Int
halveAndScale x =
let halved = x `div` 2
in case even halved of
True -> Just (halved * 10)
False -> Just (halved * 5)
processNumber :: Int -> Maybe Int
processNumber x =
case halveEven x of
Nothing -> Nothing
Just halved -> halveAndScale halvedIt may seem things only got worse, but they really haven't. Haskell already has a ready-made tool for this — the Monad class with its binding operator (bind):
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
processNumber :: Int -> Maybe Int
processNumber x = pure x >>= halveEven >>= halveAndScaleBeautiful? That's the point. There is also the so-called do notation. Here is the same example written with it:
processNumber :: Int -> Maybe Int
processNumber x = do
halved <- halveEven x
result <- halveAndScale halved
pure resultDoes this remind you of anything? The code really does look like a program in an imperative language, although its semantics stays monadic. A monad lets you explicitly control the sequence of dependent computations — that is its defining feature. And it does so beautifully.
Just like functors, monads come with laws of their own, and keeping them is the programmer's responsibility:
pure a >>= f == f a -- left identity
m >>= pure == m -- right identity
(m >>= f) >>= g == m >>= (\x -> f x >>= g) -- associativitySo much for the binding operator. Now let's see which monads show up in real code. Almost every familiar "imperative" device — exceptions, a shared config, a log, mutable state, I/O — has a pure monadic counterpart.
| Monad | Context / effect | Imperative counterpart |
|---|---|---|
| the value may be absent | null + early return | |
| an error with a cause | exceptions | |
| nondeterminism: zero or more results | nested loops | |
| shared read-only environment | config | |
| accumulating a log | logging | |
| mutable state | variables | |
| the outside world | system calls: files, network, console |
At first glance these monads solve completely different problems. But Reader, Writer and State are built very much alike — they are wrappers around functions or values with an extra context:
newtype Reader r a = Reader (r -> a) -- environment -> value
newtype Writer w a = Writer (a, w) -- value + log
newtype State s a = State (s -> (a, s)) -- old state -> (value, new state)The binding operator of the State monad threads the state through the chain by itself — no more writing by hand. The primitives: , .
Every executable Haskell program runs inside the IO monad: its entry point has type , and all interaction with the outside world is performed as IO actions. A value of type describes an action that, when executed, returns an . You cannot simply extract a pure value out of IO: there is no function .
So all these monads share a single interface — which is why one and the same do code works with different effects. And Haskell stays pure: effects do not happen behind your back; they are explicitly reflected in the types and bound into a controlled sequence of computations.