Why are side-effects modeled as monads in Haskell?

前端 未结 8 905
无人共我
无人共我 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 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:

    ...
    (, worldY) = f               worldX
    (, worldZ) = g  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.

提交回复
热议问题