I am learning Haskell and trying to understand Monads. I have two questions:
From what I understand, Monad is just another typeclass that declares ways to interact with data inside "containers", including Maybe
, List
, and IO
. It seems clever and clean to implement these 3 things with one concept, but really, the point is so there can be clean error handling in a chain of functions, containers, and side effects. Is this a correct interpretation?
How exactly is the problem of side-effects solved? With this concept of containers, the language essentially says anything inside the containers is non-deterministic (such as i/o). Because lists and IOs are both containers, lists are equivalence-classed with IO, even though values inside lists seem pretty deterministic to me. So what is deterministic and what has side-effects? I can't wrap my head around the idea that a basic value is deterministic, until you stick it in a container (which is no special than the same value with some other values next to it, e.g. Nothing
) and it can now be random.
Can someone explain how, intuitively, Haskell gets away with changing state with inputs and output? I'm not seeing the magic here.
Conclusion. Monad is a simple and powerful design pattern for function composition that helps us to solve very common IT problems such as input/output, exception handling, parsing, concurrency and other.
monads are used to address the more general problem of computations (involving state, input/output, backtracking, ...) returning values: they do not solve any input/output-problems directly but rather provide an elegant and flexible abstraction of many solutions to related problems.
So in simple words, a monad is a rule to pass from any type X to another type T(X) , and a rule to pass from two functions f:X->T(Y) and g:Y->T(Z) (that you would like to compose but can't) to a new function h:X->T(Z) . Which, however, is not the composition in strict mathematical sense.
Haskell is a pure language Moreover, Haskell functions can't have side effects, which means that they can't effect any changes to the "real world", like changing files, writing to the screen, printing, sending data over the network, and so on.
The point is so there can be clean error handling in a chain of functions, containers, and side effects. Is this a correct interpretation?
Not really. You've mentioned a lot of concepts that people cite when trying to explain monads, including side effects, error handling and non-determinism, but it sounds like you've gotten the incorrect sense that all of these concepts apply to all monads. But there's one concept you mentioned that does: chaining.
There are two different flavors of this, so I'll explain it two different ways: one without side effects, and one with side effects.
Take the following example:
addM :: (Monad m, Num a) => m a -> m a -> m a addM ma mb = do a <- ma b <- mb return (a + b)
This function adds two numbers, with the twist that they are wrapped in some monad. Which monad? Doesn't matter! In all cases, that special do
syntax de-sugars to the following:
addM ma mb = ma >>= \a -> mb >>= \b -> return (a + b)
... or, with operator precedence made explicit:
ma >>= (\a -> mb >>= (\b -> return (a + b)))
Now you can really see that this is a chain of little functions, all composed together, and its behavior will depend on how >>=
and return
are defined for each monad. If you're familiar with polymorphism in object-oriented languages, this is essentially the same thing: one common interface with multiple implementations. It's slightly more mind-bending than your average OOP interface, since the interface represents a computation policy rather than, say, an animal or a shape or something.
Okay, let's see some examples of how addM
behaves across different monads. The Identity
monad is a decent place to start, since its definition is trivial:
instance Monad Identity where return a = Identity a -- create an Identity value (Identity a) >>= f = f a -- apply f to a
So what happens when we say:
addM (Identity 1) (Identity 2)
Expanding this, step by step:
(Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b))) (\a -> (Identity 2) >>= (\b -> return (a + b)) 1 (Identity 2) >>= (\b -> return (1 + b)) (\b -> return (1 + b)) 2 return (1 + 2) Identity 3
Great. Now, since you mentioned clean error handling, let's look at the Maybe
monad. Its definition is only slightly trickier than Identity
:
instance Monad Maybe where return a = Just a -- same as Identity monad! (Just a) >>= f = f a -- same as Identity monad again! Nothing >>= _ = Nothing -- the only real difference from Identity
So you can imagine that if we say addM (Just 1) (Just 2)
we'll get Just 3
. But for grins, let's expand addM Nothing (Just 1)
instead:
Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b))) Nothing
Or the other way around, addM (Just 1) Nothing
:
(Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b))) (\a -> Nothing >>= (\b -> return (a + b)) 1 Nothing >>= (\b -> return (1 + b)) Nothing
So the Maybe
monad's definition of >>=
was tweaked to account for failure. When a function is applied to a Maybe
value using >>=
, you get what you'd expect.
Okay, so you mentioned non-determinism. Yes, the list monad can be thought of as modeling non-determinism in a sense... It's a little weird, but think of the list as representing alternative possible values: [1, 2, 3]
is not a collection, it's a single non-deterministic number that could be either one, two or three. That sounds dumb, but it starts to make some sense when you think about how >>=
is defined for lists: it applies the given function to each possible value. So addM [1, 2] [3, 4]
is actually going to compute all possible sums of those two non-deterministic values: [4, 5, 5, 6]
.
Okay, now to address your second question...
Let's say you apply addM
to two values in the IO
monad, like:
addM (return 1 :: IO Int) (return 2 :: IO Int)
You don't get anything special, just 3 in the IO
monad. addM
does not read or write any mutable state, so it's kind of no fun. Same goes for the State
or ST
monads. No fun. So let's use a different function:
fireTheMissiles :: IO Int -- returns the number of casualties
Clearly the world will be different each time missiles are fired. Clearly. Now let's say you're trying to write some totally innocuous, side effect free, non-missile-firing code. Perhaps you're trying once again to add two numbers, but this time without any monads flying around:
add :: Num a => a -> a -> a add a b = a + b
and all of a sudden your hand slips, and you accidentally typo:
add a b = a + b + fireTheMissiles
An honest mistake, really. The keys were so close together. Fortunately, because fireTheMissiles
was of type IO Int
rather than simply Int
, the compiler is able to avert disaster.
Okay, totally contrived example, but the point is that in the case of IO
, ST
and friends, the type system keeps effects isolated to some specific context. It doesn't magically eliminate side effects, making code referentially transparent that shouldn't be, but it does make it clear at compile time what scope the effects are limited to.
So getting back to the original point: what does this have to do with chaining or composition of functions? Well, in this case, it's just a handy way of expressing a sequence of effects:
fireTheMissilesTwice :: IO () fireTheMissilesTwice = do a <- fireTheMissiles print a b <- fireTheMissiles print b
A monad represents some policy for chaining computations. Identity
's policy is pure function composition, Maybe
's policy is function composition with failure propogation, IO
's policy is impure function composition and so on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With