几乎所有你可以用 monad 做的事情,你可以不用它们做。(嗯,有些是特殊的,例如ST
, STM
,IO
等,但那是另一回事。)但是:
- 它们允许您封装许多常见模式,例如在这种情况下的状态计算,并隐藏原本需要的细节或样板代码;和
- 有许多函数可以在任何(或许多)monad 上工作,您可以专门针对您正在使用的特定 monad。
举个例子:通常需要某种生成器来提供唯一名称,例如在生成代码时等。这可以使用 state monad 轻松完成:每次newName
调用时,它都会输出一个新名称并递增内部状态:
import Control.Monad.State
import Data.Tree
import qualified Data.Traversable as T
type NameGen = State Int
newName :: NameGen String
newName = state $ \i -> ("x" ++ show i, i + 1)
现在假设我们有一棵有一些缺失值的树。我们想为他们提供这样生成的名称。幸运的是,有一个通用函数mapM
允许使用任何 monad 遍历任何可遍历的结构(如果没有 monad 抽象,我们就没有这个函数)。现在修复树很容易。对于每个值,我们检查它是否被填充(然后我们只是return
用来将它提升到 monad 中),如果没有,则提供一个新名称:
fillTree :: Tree (Maybe String) -> NameGen (Tree String)
fillTree = T.mapM (maybe newName return)
想象一下在没有 monad 的情况下实现这个函数,有明确的状态 - 手动遍历树并携带状态。最初的想法将完全丢失在样板代码中。此外,该功能将非常特定于Tree
和NameGen
。
但是有了单子,我们可以走得更远。我们可以参数化名称生成器并构造更通用的函数:
fillTreeM :: (Monad m) => m String -> Tree (Maybe String) -> m (Tree String)
fillTreeM gen = T.mapM (maybe gen return)
注意第一个参数m String
。它不是一个常数值,它是在需要时String
生成新的 inside 的秘诀。String
m
那么原来的可以改写为
fillTree' :: Tree (Maybe String) -> NameGen (Tree String)
fillTree' = fillTreeM newName
但是现在我们可以将同一个函数用于许多非常不同的目的。例如,使用Rand
monad并提供随机生成的名称。
或者,在某些时候,我们可能会认为没有填写节点的树是无效的。然后我们只是说,无论何时我们被要求提供一个新名称,我们都会中止整个计算。这可以实现为
checkTree :: Tree (Maybe String) -> Maybe (Tree String)
checkTree = fillTreeM Nothing
这里Nothing
是 type Maybe String
,而不是尝试生成新名称,而是中止Maybe
monad 中的计算。
如果没有单子的概念,这种抽象级别几乎是不可能的。