小编典典

为什么副作用在 Haskell 中被建模为 monad?

all

谁能给出一些关于为什么 Haskell 中的不纯计算被建模为 monad 的指示?

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


阅读 153

收藏
2022-08-02

共1个答案

小编典典

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

因此,对于不纯函数

f' :: Int -> Int

我们将 RealWorld 添加到考虑中

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,直接写就可以了

f :: Int -> IO Int

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

“不纯”函数的组成

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

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

我们想

  • *从控制台 *获取文件名,
  • 读取 该文件,然后
  • 将该文件的内容打印 到控制台。

如果我们可以访问现实世界的状态,我们将如何做到这一点?

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

我们在这里看到了一个模式。函数调用如下:

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

所以我们可以定义一个操作符~~~来绑定它们:

(~~~) :: (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

不触及现实世界。

“净化”

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

upperCase :: String -> String

但要让它进入现实世界,它必须返回一个IO String. 很容易解除这样的功能:

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 . upperCase,我们可以写

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

(注:通常我们写getLine ~~~ getContents ~~~ (putStrLn . upperCase)

我们一直在使用 monad

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

  1. 我们定义了一个(~~~) :: IO b -> (b -> IO c) -> IO c将两个不纯函数链接在一起的运算符
  2. 我们定义了一个impurify :: a -> IO a将纯值转换为不纯值的函数。

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


技术说明

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

  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

留作练习。

2022-08-02