Haskell中Monad和Applicative的区别
我刚刚从typeclassopedia中了解了Monad
和Applicative
之间的区别。 我可以理解, Applicative
没有join
。 但是下面的描述对我来说看起来含糊不清,我无法弄清楚单子计算/行为的“结果”究竟意味着什么。 所以,如果我将一个值写入Maybe
,这会产生一个monad,那么这个“计算”的结果是什么?
让我们仔细看看(>> =)的类型。 基本的直觉是它将两个计算组合成一个更大的计算。 第一个参数ma是第一个计算。 然而,如果第二个参数只是一个mb,那将是无聊的; 那么计算就没有办法与另一个进行交互(实际上,这正是Applicative的情况)。 因此,(>> =)的第二个参数具有类型a - > mb:这种类型的函数在给定第一个计算结果的情况下可以产生第二个要运行的计算。 ...直观地说,正是这种能力使用之前计算的输出来决定下一步要运行哪些计算,这使得Monad比Applicative更强大。 应用计算的结构是固定的,而Monad计算的结构可以基于中间结果而改变。
是否有一个具体的例子说明“使用前面的计算的输出来决定接下来要运行什么计算的能力”,这是Applicative没有的?
我最喜欢的例子是“纯适应性的”。 我们将首先分析Either的基本Monad实例
instance Monad (Either e) where
return = Right
Left e >>= _ = Left e
Right a >>= f = f a
这个例子嵌入了一个非常自然的短路概念:我们从左到右进行,一旦单个计算“失败”进入Left
那么所有其他的都会执行。 还有任何Monad
都有的自然Applicative
实例
instance Applicative (Either e) where
pure = return
(<*>) = ap
在return
之前, ap
只不过是从左到右排序:
ap :: Monad m => m (a -> b) -> m a -> m b
ap mf ma = do
f <- mf
a <- ma
return (f a)
现在有了这个麻烦Either
实例涉及到光,当你想收集这发生在任何地方在计算中,不知何故产生错误的摘要错误消息。 这在短路的情况下飞行。 它也面临着(>>=)
的类型
(>>=) :: m a -> (a -> m b) -> m b
如果我们将ma
看作“过去”,将mb
看作“未来”,那么只要能够运行“stepper” (a -> mb)
(>>=)
产生过去的未来。 这种“步进”要求的是价值a
真的存在于未来......这是不可能的Either
。 因此(>>=)
要求短路。
所以我们会实现一个不能有相应Monad
的Applicative
实例。
instance Monoid e => Applicative (Either e) where
pure = Right
现在(<*>)
是值得仔细考虑的特殊部分。 它在前三种情况下执行了一些“短路”,但在第四种情况下做了一些有趣的事情。
Right f <*> Right a = Right (f a) -- neutral
Left e <*> Right _ = Left e -- short-circuit
Right _ <*> Left e = Left e -- short-circuit
Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!
再次注意,如果我们将左边的论点看作“过去”,将正确的论点看作“未来”,那么(<*>)
与(>>=)
相比是特殊的,因为它允许“打开”未来,过去并行,而不一定需要“过去”的结果来计算“未来”。
这意味着,直接,我们可以用我们的纯粹Applicative
Either
收集错误,忽略Right
■如果任何Left
小号存在于链
> Right (+1) <*> Left [1] <*> Left [2]
> Left [1,2]
所以让我们把这个直觉放在头上。 我们不能用单纯的应用性做些什么Either
? 那么,由于它的运作取决于在运行过去之前检查未来,我们必须能够确定未来的结构而不依赖于过去的价值。 换句话说,我们不能写
ifA :: Applicative f => f Bool -> f a -> f a -> f a
其满足以下等式
ifA (pure True) t e == t
ifA (pure False) t e == e
而我们可以写ifM
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM mbool th el = do
bool <- mbool
if bool then th else el
这样
ifM (return True) t e == t
ifM (return False) t e == e
这不可能发生,因为ifA
体现取决于嵌入参数的计算值的计算结果正好想法。
Just 1
描述了一个“计算”,其“结果”为1. Nothing
描述没有产生结果的计算。
Monad和Applicative之间的区别在于Monad中有一个选择。 Monads的关键区别在于计算中不同路径之间的选择能力(不仅仅是早期突破)。 根据前一步计算所产生的值,计算结构的其余部分可能会改变。
这就是这个意思。 在monadic链中
return 42 >>= (x ->
if x == 1
then
return (x+1)
else
return (x-1) >>= (y ->
return (1/y) ))
if
选择什么计算构造。
在Applicative的情况下,在
pure (1/) <*> ( pure (+(-1)) <*> pure 1 )
所有的功能都在“计算内部”工作,没有机会拆分链条。 每个函数都会转换它所馈送的值。 从功能的角度来看,计算结构的“形状”完全在“外部”。
函数可以返回一个特殊值来指示失败,但是它不会导致计算中的下一步被跳过。 他们都必须以特殊的方式处理特殊的价值。 计算的形状不能根据收到的值更改。
对于monad,这些函数本身会根据他们的选择构造计算。
这是我对@J的看法。 Abrahamson的例子说明为什么ifA
不能使用例如(pure True)
的值。 从本质上讲,它仍然归结为缺少的join
从功能Monad
的Applicative
,统一了在typeclassopedia给出的解释之间的区别的两种不同的观点Monad
和Applicative
。
所以使用@J。 纯粹应用性的亚伯拉罕的例子Either
:
instance Monoid e => Applicative (Either e) where
pure = Right
Right f <*> Right a = Right (f a) -- neutral
Left e <*> Right _ = Left e -- short-circuit
Right _ <*> Left e = Left e -- short-circuit
Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!
(其具有与Either
Monad
类似的短路效应)以及ifA
功能
ifA :: Applicative f => f Bool -> f a -> f a -> f a
如果我们试图实现所提到的方程:
ifA (pure True) t e == t
ifA (pure False) t e == e
?
那么,正如已经指出的那样,最终, (pure True)
的内容不能被以后的计算使用。 但从技术上讲,这是不对的。 我们可以使用(pure True)
的内容,因为Monad
也是fmap
的Functor
。 我们可以做的:
ifA' b t e = fmap b (x -> if x then t else e)
问题在于ifA'
的返回类型,它是f (fa)
。 在Applicative
,没有办法将两个嵌套的Applicative
S合并为一个。 但是,这种折叠功能正是Monad
所join
功能。 所以,
ifA = join . ifA'
将满足ifA
的方程,如果我们可以适当地实现join
。 Applicative
在这里缺少的恰恰就是join
功能。 换句话说,我们可以以某种方式使用Applicative
以前结果的结果。 但是在Applicative
框架中这样做会涉及将返回值的类型扩展为嵌套的应用程序值,我们无法恢复到单一级别的应用程序值。 这将是一个严重的问题,因为例如我们无法适当地使用Applicative
S来编写函数。 使用join
解决这个问题,但是join
引入促进了Monad
的Applicative
。