本杰明·皮尔斯在TAPL中说
一个类型系统可以看成是计算一种静态的
程序中术语的运行时行为的近似值。
这就是为什么配备强大类型系统的语言比类型差的语言更具有严格的表达能力。你可以用同样的方式考虑单子。
正如@Carl 和sigfpe 所指出的,您可以为数据类型配备您想要的所有操作,而无需求助于单子、类型类或任何其他抽象的东西。然而,monad 不仅可以让你编写可重用的代码,还可以抽象出所有多余的细节。
例如,假设我们要过滤一个列表。最简单的方法是使用filter函数:filter (> 3) [1..10],等于[4,5,6,7,8,9,10]。
filter 的一个稍微复杂一点的版本,它也从左到右传递一个累加器,是
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
要得到所有i,这样i <= 10, sum [1..i] > 4, sum [1..i] < 25,我们可以这样写
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
等于[3,4,5,6]。
或者我们可以重新定义nub 函数,它从列表中删除重复元素,就filterAccum而言:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4] 等于 [1,2,4,5,3,8,9]。一个列表在这里作为累加器传递。该代码有效,因为它可以离开列表 monad,所以整个计算保持纯净(notElem 实际上不使用>>=,但它可以)。但是,不可能安全地离开 IO monad(即,您不能执行 IO 操作并返回纯值——该值总是被包装在 IO monad 中)。另一个例子是可变数组:在你离开 ST monad(可变数组所在的位置)后,你不能再以恒定的时间更新数组。所以我们需要来自Control.Monad 模块的一元过滤:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM 对列表中的所有元素执行单子操作,产生元素,单子操作返回 True。
带有数组的过滤示例:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
按预期打印[1,2,4,5,3,8,9]。
还有一个带有 IO monad 的版本,它询问要返回哪些元素:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
例如
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
作为最后的说明,filterAccum 可以定义为filterM:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
使用 StateT monad,它在后台使用,只是一个普通的数据类型。
这个例子说明,monad 不仅允许您抽象计算上下文和编写干净的可重用代码(由于 monad 的可组合性,正如 @Carl 所解释的那样),而且还可以统一处理用户定义的数据类型和内置原语.