谁能给出一些关于为什么 Haskell 中的不纯计算被建模为 monad 的指示?
我的意思是 monad 只是一个具有 4 个操作的接口,那么在其中建模副作用的原因是什么?
假设一个函数有副作用。如果我们把它产生的所有效果都作为输入和输出参数,那么这个函数对外界来说是纯粹的。
因此,对于不纯函数
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
type IO a = RealWorld -> (a, 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. 很容易解除这样的功能:
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,我们可以写
impureUpperCase = impurify . upperCase
printUpperCaseFile = getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(注:通常我们写getLine ~~~ getContents ~~~ (putStrLn . upperCase))
getLine ~~~ getContents ~~~ (putStrLn . upperCase)
现在让我们看看我们做了什么:
(~~~) :: IO b -> (b -> IO c) -> IO c
impurify :: a -> IO a
现在我们进行识别(>>=) = (~~~)和return = impurify,看看?我们有一个单子。
(>>=) = (~~~)
return = impurify
为了确保它真的是一个 monad,还有一些公理也需要检查:
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 -> g x >>= h) = (f >>= g) >>= h
留作练习。