如果一个类型m :: * -> *
有一个Monad
实例,你会得到带有 type 的图灵完备的函数组合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
. 优化认知开销对用户来说更友好。
现在,让我们看看这些功能与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
。毕竟,它严格来说更强大。但有趣的是你失去了什么。您将失去组合函数的能力,这些函数会根据输入来改变它们的效果。也就是说,你失去了编写某些控制流模式的能力,这些模式组成了带有 types 的函数a -> f b
。
图灵完备的组合正是让Monad
界面变得有趣的原因。如果它不允许图灵完备的组合,那么程序员就不可能在IO
没有为你很好地预先打包的任何特定控制流中组合动作。事实上,您可以使用Monad
原语来表达任何控制流,这使得该IO
类型成为在 Haskell 中管理 IO 问题的可行方法。
更多的类型不仅仅是IO
语义上有效的Monad
接口。碰巧的是,Haskell 具有对整个接口进行抽象的语言工具。由于这些因素,Monad
它是一个有价值的类,可以在可能的情况下为其提供实例。这样做可以让您访问为处理一元类型而提供的所有现有抽象功能,而不管具体类型是什么。
因此,如果 Haskell 程序员似乎总是关心Monad
一个类型的实例,那是因为它是可以提供的最通用的实例。