如果一个类型
m :: * -> *
有一个
Monad
实例,那么你就可以使用函数组合的方式实现具有类型
a -> m b
的图灵完备性。这是一个非常有用的属性。你可以将各种图灵完备的控制流程抽象出来,使其不再与特定的含义相关。这是一个最小的组合模式,支持抽象任何控制流程以适应支持该模式的类型。
相比之下,例如在Applicative
里,你只能获得等效于推动式自动机的计算能力的组合模式。当然,更多类型支持具有更有限的能力的组合。当你限制可用的功能时,你可以进行额外的优化。这两个原因是为什么Applicative
类存在并且很有用的原因。但通常可以成为Monad
实例的东西,这样类型的用户就可以执行最通用的操作。
编辑:
根据广大读者要求,以下是一些使用Monad
类的函数:
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= \z -> if z then x else y
whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
(*&&) :: Monad m => m Bool -> m Bool -> m Bool
x *&& y = ifM x y (return False)
(*||) :: Monad m => m Bool -> m Bool -> m Bool
x *|| y = ifM x (return True) y
notM :: Monad m => m Bool -> m Bool
notM x = x >>= return . not
结合使用do语法(或原始的>>=
运算符)可以实现名称绑定,无限循环和完整的布尔逻辑。这是一个众所周知的原语集,足以实现图灵完备性。请注意,所有函数都已被提升以在单子值上工作,而不是简单的值上。只有在必要时才绑定所有单子效应-仅将ifM
选择分支的效应绑定到其最终值中。*&&
和*||
在可能的情况下忽略其第二个参数。依此类推..
现在,那些类型签名可能不涉及每个单子操作数的功能,但那只是认知简化。如果所有非函数参数和结果都更改为() -> m a
,则除了bottom之外,不会有语义差异。它只是友好地优化了用户的认知负担。
现在,让我们看看那些具有Applicative
接口的函数会发生什么。
ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y
嗯,它有同样的类型签名。但是这里已经有一个很大的问题了。无论选择哪个值,x和y的影响都会绑定到组合结构中。
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)
好的,那似乎还不错,除了它是一个无限循环,因为ifA
总会执行两个分支...除非它甚至没有那么接近。pure x
的类型是f a
。而whileA p step <$> step x
的类型是f (f a)
。这甚至不是一个无限循环。这是一个编译错误。让我们再试一次。
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
哎呀,这可糟了。甚至没能完成第一步。whileA p step
的类型是a -> f a
。如果你尝试将其用作<*>
的第一个参数,它会获取顶层类型构造函数(即(->)
)的Applicative
实例,而不是f
的实例。是的,这也不起作用。
实际上,从我的Monad
示例中,唯一可以与Applicative
接口一起使用的函数是notM
。这个特定的函数只需要Functor
接口就可以正常工作。剩下的都失败了。
当然,使用Monad
接口编写代码并不能使用Applicative
接口所不能的。毕竟,Monad
更加强大。但有趣的是你失去了什么。你失去了基于输入更改效果的函数组合能力。也就是说,你失去了编写具有a -> f b
类型的函数组合的某些控制流模式的能力。
图灵完备的组合正是使Monad
接口变得有趣的原因。如果它不允许图灵完备的组合,作为程序员,你将无法组合在任何特定控制流中使用IO
动作。正是使用Monad
原语来表达任何控制流,使得IO
类型成为Haskell管理IO问题的可行方式。
除了IO
之外,许多类型都具有语义上有效的Monad
接口。而且恰巧Haskell具有抽象整个接口的语言功能。由于这些因素,Monad
是提供实例时非常有价值的类。这样做可以让你访问所有现有的与单子类型一起使用的抽象功能,而不管具体类型是什么。
所以如果Haskell程序员似乎总是关心某个类型的Monad
实例,那是因为它是最通用的实例。