Why monads? How does it resolve side-effects?

后端 未结 7 855
感情败类
感情败类 2020-12-22 23:23

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 i

相关标签:
7条回答
  • 2020-12-22 23:32

    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.

    0 讨论(0)
  • 2020-12-22 23:37

    You could see a given monad m as a set/family (or realm, domain, etc.) of actions (think of a C statement). The monad m defines the kind of (side-)effects that its actions may have:

    • with [] you can define actions which can fork their executions in different "independent parallel worlds";
    • with Either Foo you can define actions which can fail with errors of type Foo;
    • with IO you can define actions which can have side-effects on the "outside world" (access files, network, launch processes, do a HTTP GET ...);
    • you can have a monad whose effect is "randomness" (see package MonadRandom);
    • you can define a monad whose actions can make a move in a game (say chess, Go…) and receive move from an opponent but are not able to write to your filesystem or anything else.

    Summary

    If m is a monad, m a is an action which produces a result/output of type a.

    The >> and >>= operators are used to create more complex actions out of simpler ones:

    • a >> b is a macro-action which does action a and then action b;
    • a >> a does action a and then action a again;
    • with >>= the second action can depend on the output of the first one.

    The exact meaning of what an action is and what doing an action and then another one is depends on the monad: each monad defines an imperative sublanguage with some features/effects.

    Simple sequencing (>>)

    Let's say with have a given monad M and some actions incrementCounter, decrementCounter, readCounter:

    instance M Monad where ...
    
    -- Modify the counter and do not produce any result:
    incrementCounter :: M ()
    decrementCounter :: M ()
    
    -- Get the current value of the counter
    readCounter :: M Integer
    

    Now we would like to do something interesting with those actions. The first thing we would like to do with those actions is to sequence them. As in say C, we would like to be able to do:

    // This is C:
    counter++;
    counter++;
    

    We define an "sequencing operator" >>. Using this operator we can write:

    incrementCounter >> incrementCounter
    

    What is the type of "incrementCounter >> incrementCounter"?

    1. It is an action made of two smaller actions like in C you can write composed-statements from atomic statements :

      // This is a macro statement made of several statements
      {
        counter++;
        counter++;
      }
      
      // and we can use it anywhere we may use a statement:
      if (condition) {
         counter++;
         counter++;     
      }
      
    2. it can have the same kind of effects as its subactions;

    3. it does not produce any output/result.

    So we would like incrementCounter >> incrementCounter to be of type M (): an (macro-)action with the same kind of possible effects but without any output.

    More generally, given two actions:

    action1 :: M a
    action2 :: M b
    

    we define a a >> b as the macro-action which is obtained by doing (whatever that means in our domain of action) a then b and produces as output the result of the execution of the second action. The type of >> is:

    (>>) :: M a -> M b -> M b
    

    or more generally:

    (>>) :: (Monad m) => m a -> m b -> m b
    

    We can define bigger sequence of actions from simpler ones:

     action1 >> action2 >> action3 >> action4
    

    Input and outputs (>>=)

    We would like to be able to increment by something else that 1 at a time:

    incrementBy 5
    

    We want to provide some input in our actions, in order to do this we define a function incrementBy taking an Int and producing an action:

    incrementBy :: Int -> M ()
    

    Now we can write things like:

    incrementCounter >> readCounter >> incrementBy 5
    

    But we have no way to feed the output of readCounter into incrementBy. In order to do this, a slightly more powerful version of our sequencing operator is needed. The >>= operator can feed the output of a given action as input to the next action. We can write:

    readCounter >>= incrementBy
    

    It is an action which executes the readCounter action, feeds its output in the incrementBy function and then execute the resulting action.

    The type of >>= is:

    (>>=) :: Monad m => m a -> (a -> m b) -> m b
    

    A (partial) example

    Let's say I have a Prompt monad which can only display informations (text) to the user and ask informations to the user:

    -- We don't have access to the internal structure of the Prompt monad
    module Prompt (Prompt(), echo, prompt) where
    
    -- Opaque
    data Prompt a = ...
    instance Monad Prompt where ...
    
    -- Display a line to the CLI:
    echo :: String -> Prompt ()
    
    -- Ask a question to the user:
    prompt :: String -> Prompt String
    

    Let's try to define a promptBoolean message actions which asks for a question and produces a boolean value.

    We use the prompt (message ++ "[y/n]") action and feed its output to a function f:

    • f "y" should be an action which does nothing but produce True as output;

    • f "n" should be an action which does nothing but produce False as output;

    • anything else should restart the action (do the action again);

    promptBoolean would look like this:

        -- Incomplete version, some bits are missing:
        promptBoolean :: String -> M Boolean
        promptBoolean message = prompt (message ++ "[y/n]") >>= f
          where f result = if result == "y"
                           then ???? -- We need here an action which does nothing but produce `True` as output
                           else if result=="n"
                                then ???? -- We need here an action which does nothing but produce `False` as output
                                else echo "Input not recognised, try again." >> promptBoolean
    

    Producing a value without effect (return)

    In order to fill the missing bits in our promptBoolean function, we need a way to represent dummy actions without any side effect but which only outputs a given value:

    -- "return 5" is an action which does nothing but outputs 5
    return :: (Monad m) => a -> m a
    

    and we can now write out promptBoolean function:

    promptBoolean :: String -> Prompt Boolean
    promptBoolean message :: prompt (message ++ "[y/n]") >>= f
      where f result = if result=="y"
                       then return True
                         else if result=="n"
                         then return False
                         else echo "Input not recognised, try again." >> promptBoolean message
    

    By composing those two simple actions (promptBoolean, echo) we can define any kind of dialogue between the user and your program (the actions of the program are deterministic as our monad does not have a "randomness effect").

    promptInt :: String -> M Int
    promptInt = ... -- similar
    
    -- Classic "guess a number game/dialogue"
    guess :: Int -> m()
    guess n = promptInt "Guess:" m -> f
       where f m = if m == n
                   then echo "Found"
                   else (if m > n
                         then echo "Too big"
                         then echo "Too small") >> guess n       
    

    The operations of a monad

    A Monad is a set of actions which can be composed with the return and >>= operators:

    • >>= for action composition;

    • return for producing a value without any (side-)effect.

    These two operators are the minimal operators needed to define a Monad.

    In Haskell, the >> operator is needed as well but it can in fact be derived from >>=:

    (>>): Monad m => m a -> m b -> m b
    a >> b = a >>= f
     where f x = b
    

    In Haskell, an extra fail operator is need as well but this is really a hack (and it might be removed from Monad in the future).

    This is the Haskell definition of a Monad:

    class Monad m where     
      return :: m a     
      (>>=) :: m a -> (a -> m b) -> m b     
      (>>) :: m a -> m b -> m b  -- can be derived from (>>=)
      fail :: String -> m a      -- mostly a hack
    

    Actions are first-class

    One great thing about monads is that actions are first-class. You can take them in a variable, you can define function which take actions as input and produce some other actions as output. For example, we can define a while operator:

    -- while x y : does action y while action x output True
    while :: (Monad m) => m Boolean -> m a -> m ()
    while x y = x >>= f
      where f True = y >> while x y
            f False = return ()
    

    Summary

    A Monad is a set of actions in some domain. The monad/domain define the kind of "effects" which are possible. The >> and >>= operators represent sequencing of actions and monadic expression may be used to represent any kind of "imperative (sub)program" in your (functional) Haskell program.

    The great things are that:

    • you can design your own Monad which supports the features and effects that you want

      • see Prompt for an example of a "dialogue only subprogram",

      • see Rand for an example of "sampling only subprogram";

    • you can write your own control structures (while, throw, catch or more exotic ones) as functions taking actions and composing them in some way to produce a bigger macro-actions.

    MonadRandom

    A good way of understanding monads, is the MonadRandom package. The Rand monad is made of actions whose output can be random (the effect is randomness). An action in this monad is some kind of random variable (or more exactly a sampling process):

     -- Sample an Int from some distribution
     action :: Rand Int
    

    Using Rand to do some sampling/random algorithms is quite interesting because you have random variables as first class values:

    -- Estimate mean by sampling nsamples times the random variable x
    sampleMean :: Real a => Int -> m a -> m a
    sampleMean n x = ...
    

    In this setting, the sequence function from Prelude,

     sequence :: Monad m => [m a] -> m [a]
    

    becomes

     sequence :: [Rand a] -> Rand [a]
    

    It creates a random variable obtained by sampling independently from a list of random variables.

    0 讨论(0)
  • 2020-12-22 23:41

    There are three main observations concerning the IO monad:

    1) You can't get values out of it. Other types like Maybe might allow to extract values, but neither the monad class interface itself nor the IO data type allow it.

    2) "Inside" IO is not only the real value but also that "RealWorld" thing. This dummy value is used to enforce the chaining of actions by the type system: If you have two independent calculations, the use of >>= makes the second calculation dependent on the first.

    3) Assume a non-deterministic thing like random :: () -> Int, which isn't allowed in Haskell. If you change the signature to random :: Blubb -> (Blubb, Int), it is allowed, if you make sure that nobody ever can use a Blubb twice: Because in that case all inputs are "different", it is no problem that the outputs are different as well.

    Now we can use the fact 1): Nobody can get something out of IO, so we can use the RealWord dummy hidden in IO to serve as a Blubb. There is only one IOin the whole application (the one we get from main), and it takes care of proper sequentiation, as we have seen in 2). Problem solved.

    0 讨论(0)
  • 2020-12-22 23:51

    the point is so there can be clean error handling in a chain of functions, containers, and side effects

    More or less.

    how exactly is the problem of side-effects solved?

    A value in the I/O monad, i.e. one of type IO a, should be interpreted as a program. p >> q on IO values can then be interpreted as the operator that combines two programs into one that first executes p, then q. The other monad operators have similar interpretations. By assigning a program to the name main, you declare to the compiler that that is the program that has to be executed by its output object code.

    As for the list monad, it's not really related to the I/O monad except in a very abstract mathematical sense. The IO monad gives deterministic computation with side effects, while the list monad gives non-deterministic (but not random!) backtracking search, somewhat similar to Prolog's modus operandi.

    0 讨论(0)
  • 2020-12-22 23:54

    One thing that often helps me to understand the nature of something is to examine it in the most trivial way possible. That way, I'm not getting distracted by potentially unrelated concepts. With that in mind, I think it may be helpful to understand the nature of the Identity Monad, as it's the most trivial implementation of a Monad possible (I think).

    What is interesting about the Identity Monad? I think it is that it allows me to express the idea of evaluating expressions in a context defined by other expressions. And to me, that is the essence of every Monad I've encountered (so far).

    If you already had a lot of exposure to 'mainstream' programming languages before learning Haskell (like I did), then this doesn't seem very interesting at all. After all, in a mainstream programming language, statements are executed in sequence, one after the other (excepting control-flow constructs, of course). And naturally, we can assume that every statement is evaluated in the context of all previously executed statements and that those previously executed statements may alter the environment and the behavior of the currently executing statement.

    All of that is pretty much a foreign concept in a functional, lazy language like Haskell. The order in which computations are evaluated in Haskell is well-defined, but sometimes hard to predict, and even harder to control. And for many kinds of problems, that's just fine. But other sorts of problems (e.g. IO) are hard to solve without some convenient way to establish an implicit order and context between the computations in your program.

    As far as side-effects go, specifically, often they can be transformed (via a Monad) in to simple state-passing, which is perfectly legal in a pure functional language. Some Monads don't seem to be of that nature, however. Monads such as the IO Monad or the ST monad literally perform side-effecting actions. There are many ways to think about this, but one way that I think about it is that just because my computations must exist in a world without side-effects, the Monad may not. As such, the Monad is free to establish a context for my computation to execute that is based on side-effects defined by other computations.

    Finally, I must disclaim that I am definitely not a Haskell expert. As such, please understand that everything I've said is pretty much my own thoughts on this subject and I may very well disown them later when I understand Monads more fully.

    0 讨论(0)
  • 2020-12-22 23:55

    With this concept of containers, the language essentially says anything inside the containers is non-deterministic

    No. Haskell is deterministic. If you ask for integer addition 2+2 you will always get 4.

    "Nondeterministic" is only a metaphor, a way of thinking. Everything is deterministic under the hood. If you have this code:

    do x <- [4,5]
       y <- [0,1]
       return (x+y)
    

    it is roughly equivalent to Python code

     l = []
     for x in [4,5]:
         for y in [0,1]:
             l.append(x+y)
    

    You see nondeterminism here? No, it's deterministic construction of a list. Run it twice, you'll get the same numbers in the same order.

    You can describe it this way: Choose arbitrary x from [4,5]. Choose arbitrary y from [0,1]. Return x+y. Collect all possible results.

    That way seems to involve nondeterminism, but it's only a nested loop (list comprehension). There is no "real" nondeterminism here, it's simulated by checking all possibilities. Nondeterminism is an illusion. The code only appears to be nondeterministic.

    This code using State monad:

    do put 0
       x <- get
       put (x+2)
       y <- get
       return (y+3)
    

    gives 5 and seems to involve changing state. As with lists it's an illusion. There are no "variables" that change (as in imperative languages). Everything is nonmutable under the hood.

    You can describe the code this way: put 0 to a variable. Read the value of a variable to x. Put (x+2) to the variable. Read the variable to y, and return y+3.

    That way seems to involve state, but it's only composing functions passing additional parameter. There is no "real" mutability here, it's simulated by composition. Mutability is an illusion. The code only appears to be using it.

    Haskell does it this way: you've got functions

       a -> s -> (b,s)
    

    This function takes and old value of state and returns new value. It does not involve mutability or change variables. It's a function in mathematical sense.

    For example the function "put" takes new value of state, ignores current state and returns new state:

       put x _ = ((), x)
    

    Just like you can compose two normal functions

      a -> b
      b -> c
    

    into

      a -> c
    

    using (.) operator you can compose "state" transformers

      a -> s -> (b,s)
      b -> s -> (c,s)
    

    into a single function

      a -> s -> (c,s)
    

    Try writing the composition operator yourself. This is what really happens, there are no "side effects" only passing arguments to functions.

    0 讨论(0)
提交回复
热议问题