为什么副作用在 Haskell 中被建模为 monad? [英] Why are side-effects modeled as monads in Haskell?

查看:35
本文介绍了为什么副作用在 Haskell 中被建模为 monad?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有人能指出为什么 Haskell 中的不纯计算被建模为 monad 吗?

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

我的意思是 monad 只是一个有 4 个操作的接口,那么在其中建模副作用的原因是什么?

I mean monad is just an interface with 4 operations, so what was the reasoning to modelling side-effects in it?

推荐答案

假设一个函数有副作用.如果我们把它产生的所有效果都作为输入和输出参数,那么这个函数对外界来说就是纯粹的.

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

我们将 RealWorld 添加到考虑中

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.

那么 f 又是纯的.我们定义了一个参数化的数据类型type IO a = RealWorld ->(a, RealWorld),这样我们就不需要多次输入RealWorld,直接写

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

对于程序员来说,直接处理 RealWorld 太危险了——特别是,如果程序员得到 RealWorld 类型的值,他们可能会尝试复制它,这基本上是不可能的.(比如尝试复制整个文件系统.你会把它放在哪里?)因此,我们对 IO 的定义也封装了整个世界的状态.

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.

如果我们不能将它们链接在一起,这些不纯的函数就毫无用处.考虑

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)

我们想要

  • 从控制台获取一个文件名,
  • 阅读那个文件,然后
  • 打印该文件的内容到控制台.
  • 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

然后我们可以简单地写

printFile = getLine ~~~ getContents ~~~ putStrLn

不接触现实世界.

现在假设我们也想让文件内容大写.大写是纯函数

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

upperCase :: String -> String

但是为了让它进入现实世界,它必须返回一个IO 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)

这可以概括:

impurify :: a -> IO a

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

所以 impureUpperCase = impurify .大写,我们可以写

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

(注意:一般我们写getLine~~~getContents~~~(putStrLn.upperCase))

现在让我们看看我们做了什么:

Now let's see what we've done:

  1. 我们定义了一个操作符(~~~) :: IO b ->(b -> 10 c) ->IO c 将两个不纯函数链接在一起
  2. 我们定义了一个函数 impurify :: a ->IO a 将纯值转换为不纯值.
  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.

现在我们进行识别(>>=) = (~~~)return = impurify,看看?我们有一个 monad.

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

为了确保它真的是一个 monad,还有一些公理需要检查:

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

  • 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
    

  • f >>= (x -> gx >>= h) = (f >>= g) >>= h

    留作练习.

    这篇关于为什么副作用在 Haskell 中被建模为 monad?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆