Why are side-effects modeled as monads in Haskell?

前端 未结 8 884
无人共我
无人共我 2020-12-02 03:30

Could anyone give some pointers on why the impure computations in Haskell are modelled as monads?

I mean monad is just an interface with 4 operations, so what was th

相关标签:
8条回答
  • 2020-12-02 03:50

    AFAIK, the reason is to be able to include side effects checks in the type system. If you want to know more, listen to those SE-Radio episodes: Episode 108: Simon Peyton Jones on Functional Programming and Haskell Episode 72: Erik Meijer on LINQ

    0 讨论(0)
  • 2020-12-02 04:05

    Above there are very good detailed answers with theoretical background. But I want to give my view on IO monad. I am not experienced haskell programmer, so May be it is quite naive or even wrong. But i helped me to deal with IO monad to some extent (note, that it do not relates to other monads).

    First I want to say, that example with "real world" is not too clear for me as we cannot access its (real world) previous states. May be it do not relates to monad computations at all but it is desired in the sense of referential transparency, which is generally presents in haskell code.

    So we want our language (haskell) to be pure. But we need input/output operations as without them our program cannot be useful. And those operations cannot be pure by their nature. So the only way to deal with this we have to separate impure operations from the rest of code.

    Here monad comes. Actually, I am not sure, that there cannot exist other construct with similar needed properties, but the point is that monad have these properties, so it can be used (and it is used successfully). The main property is that we cannot escape from it. Monad interface do not have operations to get rid of the monad around our value. Other (not IO) monads provide such operations and allow pattern matching (e.g. Maybe), but those operations are not in monad interface. Another required property is ability to chain operations.

    If we think about what we need in terms of type system, we come to the fact that we need type with constructor, which can be wrapped around any vale. Constructor must be private, as we prohibit escaping from it(i.e. pattern matching). But we need function to put value into this constructor (here return comes to mind). And we need the way to chain operations. If we think about it for some time, we will come to the fact, that chaining operation must have type as >>= has. So, we come to something very similar to monad. I think, if we now analyze possible contradictory situations with this construct, we will come to monad axioms.

    Note, that developed construct do not have anything in common with impurity. It only have properties, which we wished to have to be able to deal with impure operations, namely, no-escaping, chaining, and a way to get in.

    Now some set of impure operations is predefined by the language within this selected monad IO. We can combine those operations to create new unpure operations. And all those operations will have to have IO in their type. Note however, that presence of IO in type of some function do not make this function impure. But as I understand, it is bad idea to write pure functions with IO in their type, as it was initially our idea to separate pure and impure functions.

    Finally, I want to say, that monad do not turn impure operations into pure ones. It only allows to separate them effectively. (I repeat, that it is only my understanding)

    0 讨论(0)
  • 2020-12-02 04:07

    As I understand it, someone called Eugenio Moggi first noticed that a previously obscure mathematical construct called a "monad" could be used to model side effects in computer languages, and hence specify their semantics using Lambda calculus. When Haskell was being developed there were various ways in which impure computations were modelled (see Simon Peyton Jones' "hair shirt" paper for more details), but when Phil Wadler introduced monads it rapidly became obvious that this was The Answer. And the rest is history.

    0 讨论(0)
  • 2020-12-02 04:12

    Suppose a function has side effects. If we take all the effects it produces as the input and output parameters, then the function is pure to the outside world.

    So, for an impure function

    f' :: Int -> Int
    

    we add the RealWorld to the consideration

    f :: Int -> RealWorld -> (Int, RealWorld)
    -- input some states of the whole world,
    -- modify the whole world because of the side effects,
    -- then return the new world.
    

    then f is pure again. We define a parametrized data type type IO a = RealWorld -> (a, RealWorld), so we don't need to type RealWorld so many times, and can just write

    f :: Int -> IO Int
    

    To the programmer, handling a RealWorld directly is too dangerous—in particular, if a programmer gets their hands on a value of type RealWorld, they might try to copy it, which is basically impossible. (Think of trying to copy the entire filesystem, for example. Where would you put it?) Therefore, our definition of IO encapsulates the states of the whole world as well.

    Composition of "impure" functions

    These impure functions are useless if we can't chain them together. Consider

    getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
    getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
    putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)
    

    We want to

    • get a filename from the console,
    • read that file, and
    • print that file's contents to the console.

    How would we do it if we could access the real world states?

    printFile :: RealWorld -> ((), RealWorld)
    printFile world0 = let (filename, world1) = getLine world0
                           (contents, world2) = (getContents filename) world1 
                       in  (putStrLn contents) world2 -- results in ((), world3)
    

    We see a pattern here. The functions are called like this:

    ...
    (<result-of-f>, worldY) = f               worldX
    (<result-of-g>, worldZ) = g <result-of-f> worldY
    ...
    

    So we could define an operator ~~~ to bind them:

    (~~~) :: (IO b) -> (b -> IO c) -> IO c
    
    (~~~) ::      (RealWorld -> (b,   RealWorld))
          ->                    (b -> RealWorld -> (c, RealWorld))
          ->      (RealWorld                    -> (c, RealWorld))
    (f ~~~ g) worldX = let (resF, worldY) = f worldX
                       in g resF worldY
    

    then we could simply write

    printFile = getLine ~~~ getContents ~~~ putStrLn
    

    without touching the real world.

    "Impurification"

    Now suppose we want to make the file content uppercase as well. Uppercasing is a pure function

    upperCase :: String -> String
    

    But to make it into the real world, it has to return an IO String. It is easy to lift such a function:

    impureUpperCase :: String -> RealWorld -> (String, RealWorld)
    impureUpperCase str world = (upperCase str, world)
    

    This can be generalized:

    impurify :: a -> IO a
    
    impurify :: a -> RealWorld -> (a, RealWorld)
    impurify a world = (a, world)
    

    so that impureUpperCase = impurify . upperCase, and we can write

    printUpperCaseFile = 
        getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
    

    (Note: Normally we write getLine ~~~ getContents ~~~ (putStrLn . upperCase))

    We were working with monads all along

    Now let's see what we've done:

    1. We defined an operator (~~~) :: IO b -> (b -> IO c) -> IO c which chains two impure functions together
    2. We defined a function impurify :: a -> IO a which converts a pure value to impure.

    Now we make the identification (>>=) = (~~~) and return = impurify, and see? We've got a monad.


    Technical note

    To ensure it's really a monad, there's still a few axioms which need to be checked too:

    1. return a >>= f = f a

       impurify a                =  (\world -> (a, world))
      (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                    in f resF worldY
                                 =  let (resF, worldY) =            (a, worldX)       
                                    in f resF worldY
                                 =  f a worldX
      
    2. f >>= return = f

      (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                  in impurify resF worldY
                               =  let (resF, worldY) = f worldX      
                                  in (resF, worldY)
                               =  f worldX
      
    3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

      Left as exercise.

    0 讨论(0)
  • 2020-12-02 04:12

    It's actually quite a clean way to think of I/O in a functional way.

    In most programming languages, you do input/output operations. In Haskell, imagine writing code not to do the operations, but to generate a list of the operations that you would like to do.

    Monads are just pretty syntax for exactly that.

    If you want to know why monads as opposed to something else, I guess the answer is that they're the best functional way to represent I/O that people could think of when they were making Haskell.

    0 讨论(0)
  • 2020-12-02 04:13

    Could anyone give some pointers on why the unpure computations in Haskell are modeled as monads?

    This question contains a widespread misunderstanding. Impurity and Monad are independent notions. Impurity is not modeled by Monad. Rather, there are a few data types, such as IO, that represent imperative computation. And for some of those types, a tiny fraction of their interface corresponds to the interface pattern called "Monad". Moreover, there is no known pure/functional/denotative explanation of IO (and there is unlikely to be one, considering the "sin bin" purpose of IO), though there is the commonly told story about World -> (a, World) being the meaning of IO a. That story cannot truthfully describe IO, because IO supports concurrency and nondeterminism. The story doesn't even work when for deterministic computations that allow mid-computation interaction with the world.

    For more explanation, see this answer.

    Edit: On re-reading the question, I don't think my answer is quite on track. Models of imperative computation do often turn out to be monads, just as the question said. The asker might not really assume that monadness in any way enables the modeling of imperative computation.

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