Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why monads? How does it resolve side-effects?

Tags:

haskell

monads

I am learning Haskell and trying to understand Monads. I have two questions:

  1. 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?

  2. 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.

like image 384
Oliver Zheng Avatar asked Oct 20 '11 17:10

Oliver Zheng


People also ask

What problem do monads solve?

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.

Why do we need monads?

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.

How do monads work?

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.

How does Haskell handle side effects?

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.


1 Answers

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.

No 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...

Side Effects:

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 

Summary:

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.

like image 69
mergeconflict Avatar answered Sep 19 '22 11:09

mergeconflict