Haskell中Monad与Applicative的区别

tin*_*lyx 50 monads haskell applicative

我刚才读从以下typeclassopedia约之间的差异MonadApplicative.我能理解,没有joinApplicative.但是下面的描述看起来含糊不清,我无法弄清楚monadic计算/动作的"结果"究竟是什么意思.所以,如果我把一个值放入Maybe,这使得一个monad,这个"计算"的结果是什么?

让我们更仔细地看一下(>> =)的类型.基本的直觉是它将两个计算组合成一个更大的计算.第一个参数ma是第一个计算.但是,如果第二个论点只是一个mb,那将是无聊的; 那么计算就无法相互交互(实际上,这正是Applicative的情况).因此,(>> =)的第二个参数具有类型a - > mb:这种类型的函数,给定第一次计算的结果,可以产生第二个要运行的计算....直观地说,正是这种能力使用先前计算的输出来决定接下来运行哪些计算使Monad比Applicative更强大.应用计算的结构是固定的,而Monad计算的结构可以基于中间结果而改变.

是否有一个具体的例子说明"能够使用先前计算的输出来决定接下来要运行的计算",Applicative没有?

J. *_*son 64

我最喜欢的例子是"纯粹适用于任何一个".我们首先分析Either的基础Monad实例

instance Monad (Either e) where
  return = Right
  Left e  >>= _ = Left e
  Right a >>= f = f a
Run Code Online (Sandbox Code Playgroud)

这个例子嵌入了一个非常自然的短路概念:我们从左到右进行,一旦单个计算"失败"进入Left其他所有其他的计算.Applicative任何人Monad都有自然的例子

instance Applicative (Either e) where
  pure  = return
  (<*>) = ap
Run Code Online (Sandbox Code Playgroud)

在以下ap之前,只有从左到右的排序return:

ap :: Monad m => m (a -> b) -> m a -> m b
ap mf ma = do 
  f <- mf
  a <- ma
  return (f a)
Run Code Online (Sandbox Code Playgroud)

现在,Either当您想要收集计算中出现的错误消息并以某种方式产生错误摘要时,此实例会出现问题.面对短路,这种情况很严重.它也面对这种类型(>>=)

(>>=) :: m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

如果我们将其m a视为"过去"和m b"未来",那么(>>=)只要它可以运行"步进器",就能从过去产生未来(a -> m b).这种"步进"要求a未来真正存在的价值......这是不可能的Either.因此(>>=) 要求短路.

因此,我们将实现一个Applicative无法对应的实例Monad.

instance Monoid e => Applicative (Either e) where
  pure = Right
Run Code Online (Sandbox Code Playgroud)

现在实施(<*>)是值得仔细考虑的特殊部分.它在前三种情况下执行了一些"短路" ,但在第四种情况下做了一些有趣的事情.

  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!
Run Code Online (Sandbox Code Playgroud)

再次注意,如果我们认为左边的参数是"过去"而右边的参数是"未来",那么它(<*>)是特殊的,(>>=)因为它允许"开放"未来和过去并行而不是必然需要来自"过去"是为了计算"未来".

这意味着,直接,我们可以用我们的纯粹Applicative Either收集错误,忽略Right■如果任何Left链同时存在,

> Right (+1) <*> Left [1] <*> Left [2]
> Left [1,2]
Run Code Online (Sandbox Code Playgroud)

让我们把这个直觉放在头上.我们不能用纯粹的应用程序做什么Either?那么,由于其运作取决于在运行过去之前检查未来,我们必须能够在不依赖于过去的价值的情况下确定未来的结构.换句话说,我们不能写

ifA :: Applicative f => f Bool -> f a -> f a -> f a
Run Code Online (Sandbox Code Playgroud)

满足以下等式

ifA (pure True)  t e == t
ifA (pure False) t e == e
Run Code Online (Sandbox Code Playgroud)

虽然我们可以写 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
Run Code Online (Sandbox Code Playgroud)

这样的

ifM (return True)  t e == t
ifM (return False) t e == e
Run Code Online (Sandbox Code Playgroud)

这种不可能性的产生是因为ifA根据参数计算中嵌入的值,完全体现了结果计算的思想.

  • @WillNess:它总是使用所有计算结构/运行所有效果.例如,`ifA(Just True)(Just())Nothing == Nothing`,而`ifM(Just True)(Just())Nothing == Just()`.说"我们不能写出具有预期语义*的ifA`*"可能更准确. (10认同)
  • 什么是错的`ifA tca = g <$> t <*> c**a其中gbxy = if b then x else y`? (3认同)
  • 我认为值得注意的是,当一个类型定义了一个`Monad`实例时,它的`Applicative`实例**必须与那个`Monad`实例兼容(`pure = return`,(<*>)= ap `).虽然本答案中的第二个"Applicative"实例定义满足"Applicative"定律,但它违反了此文档的要求.获得第二个"Applicative"实例的正确方法是将其定义为与"Either"同构的其他类型. (3认同)
  • 另请注意, [`Control.Applicative.Lift`](https://hackage.haskell.org/package/transformers-0.3.0.0/docs/Control-Applicative-Lift.html) 中的 `Errors` 类型精确实现了此答案中描述的“收集所有错误”行为。 (2认同)

Wil*_*ess 38

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)     ))
Run Code Online (Sandbox Code Playgroud)

if选什么样的计算来构建.

如果是Applicative,请参阅

pure (1/) <*> ( pure (+(-1)) <*> pure 1 )
Run Code Online (Sandbox Code Playgroud)

所有函数都在"内部"计算中工作,没有机会打破链条.每个函数只是转换它所输入的值.从功能的角度来看,计算结构的"形状"完全"在外部".

函数可以返回一个特殊值来指示失败,但它不能导致跳过计算中的后续步骤.他们都必须以特殊的方式处理特殊价值.计算的形状不能根据接收的值改变.

对于monad,函数本身构造了他们选择的计算.

  • 这个例子演示了一些事情_succintly_:你不仅可以像应用仿函数那样转换值,而且还可以...... 1)将计算历史存储在monadic操作链中的任何位置,2)决定如何以及何时转换根据保存的计算历史记录,(基于可能非线性的方式)值(以可能的非线性方式),3)这些一元操作体内的模型副作用,4)更简单,使用` do-block`符号. (2认同)
  • @Ivan 对于非 Haskellers 来说可能更难,但实际上要好得多。关键区别在于应用程序组合(`a &lt;*&gt; b &lt;*&gt; ...` )中涉及的所有计算描述都是预先知道的;但是使用 Monadic 组合(`a &gt;&gt;= (\ ... -&gt; b &gt;&gt;= ... )` )每个下一个计算都根据前一个计算产生的值进行计算(“依赖”)。涉及两个时间线,两个世界:一个是纯的,其中计算描述(`a`,`b` ...)被创建和组合,另一个可能是不纯的,它们“运行” - 实际计算发生的地方。 (2认同)
  • 差异的一个很好的例子是[这个答案](/sf/ask/1712746241/#24467994)。 (2认同)

tin*_*lyx 15

这是我对@J的看法.Abrahamson的例子为什么ifA不能使用例如内部的价值(pure True).本质上,它仍然可以归结为不存在的join从功能MonadApplicative,其结合在给出的两个不同的角度typeclassopedia解释之间的差值MonadApplicative.

所以使用@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!
Run Code Online (Sandbox Code Playgroud)

(具有类似的短路效果Either Monad)和ifA功能

ifA :: Applicative f => f Bool -> f a -> f a -> f a
Run Code Online (Sandbox Code Playgroud)

如果我们试图实现上述方程怎么办?

ifA (pure True)  t e == t
ifA (pure False) t e == e
Run Code Online (Sandbox Code Playgroud)

好吧,正如已经指出的那样,最终的内容(pure True)不能用于以后的计算.但从技术上讲,这是不对的.我们可以使用的内容(pure True),因为一个Monad也是Functorfmap.我们可以做的:

ifA' b t e = fmap (\x -> if x then t else e) b
Run Code Online (Sandbox Code Playgroud)

问题在于返回类型ifA',即f (f a).在Applicative,没有办法将两个嵌套的ApplicativeS 折叠成一个.但这种折叠功能恰恰是joinMonad执行.所以,

ifA = join . ifA' 
Run Code Online (Sandbox Code Playgroud)

ifA如果我们能够join适当地实施,将满足方程式.什么Applicative是缺少在这里是完全join的功能.换句话说,我们可以以某种方式使用前一个结果中的结果Applicative.但是在Applicative框架中这样做会涉及将返回值的类型扩展为嵌套的应用程序值,我们无法将其返回到单级应用程序值.这将是一个严重的问题,因为,例如,我们不能Applicative适当地使用S 组成函数.使用join修复问题,但非常介绍join促进ApplicativeMonad.


Sas*_* NF 13

可以在apvs的类型中观察到差异的关键=<<.

ap :: m (a->b) -> (m a->m b)
=<< :: (a->m b) -> (m a->m b)
Run Code Online (Sandbox Code Playgroud)

在这两种情况下都存在m a,但仅在第二种情况下m a可以决定是否应用该函数(a->m b).反过来,函数(a->m b)可以"决定"接下来绑定的函数是否被应用 - 通过生成m b不"包含" b(如[],NothingLeft).

Applicative"内部"功能中m (a->b)无法做出这样的"决定" - 它们总是产生类型的值b.

f 1 = Nothing -- here f "decides" to produce Nothing
f x = Just x

Just 1 >>= f >>= g -- g doesn't get applied, because f decided so.
Run Code Online (Sandbox Code Playgroud)

Applicative这是不可能的,所以无法显示一个例子.最接近的是:

f 1 = 0
f x = x

g <$> f <$> Just 1 -- oh well, this will produce Just 0, but can't stop g
                   -- from getting applied
Run Code Online (Sandbox Code Playgroud)


Lui*_*las 6

但下面的描述对我来说看起来很模糊,我无法弄清楚一元计算/动作的“结果”到底是什么意思。

嗯,这种模糊性在某种程度上是故意的,因为一元计算的“结果”取决于每种类型。最好的答案有点同义反复:“结果”(或结果,因为可以有多个)是实例的实现(>>=) :: Monad m => m a -> (a -> m b) -> m b调用函数参数的任何值。

那么,如果我将一个值放入Maybe,这会生成一个 monad,那么这个“计算”的结果是什么?

单子Maybe看起来像这样:

instance Monad Maybe where
    return = Just
    Nothing >>= _ = Nothing
    Just a >>= k = k a
Run Code Online (Sandbox Code Playgroud)

这里唯一有资格作为“结果”的是 的a第二个方程中的>>=,因为它是唯一被“输入”到 的第二个参数的东西>>=

其他答案已经深入探讨了ifA两者的ifM区别,所以我想我应该强调另一个显着的区别:应用程序组成,单子不组成。对于Monads,如果你想制作一个Monad结合了两个现有s的效果的a,你必须将其中一个重写为monad转换器。相反,如果您有两个,Applicatives您可以轻松地用它们制作一个更复杂的,如下所示。(代码是从 复制粘贴的transformers。)

-- | The composition of two functors.
newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose x) = Compose (fmap (fmap f) x)

-- | The composition of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Compose f g) where
    pure x = Compose (pure (pure x))
    Compose f <*> Compose x = Compose ((<*>) <$> f <*> x)


-- | The product of two functors.
data Product f g a = Pair (f a) (g a)

-- | The product of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Product f g) where
    fmap f (Pair x y) = Pair (fmap f x) (fmap f y)

-- | The product of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Product f g) where
    pure x = Pair (pure x) (pure x)
    Pair f g <*> Pair x y = Pair (f <*> x) (g <*> y)


-- | The sum of a functor @f@ with the 'Identity' functor
data Lift f a = Pure a | Other (f a)

-- | The sum of two functors is always a functor.
instance (Functor f) => Functor (Lift f) where
    fmap f (Pure x) = Pure (f x)
    fmap f (Other y) = Other (fmap f y)

-- | The sum of any applicative with 'Identity' is also an applicative 
instance (Applicative f) => Applicative (Lift f) where
    pure = Pure
    Pure f <*> Pure x = Pure (f x)
    Pure f <*> Other y = Other (f <$> y)
    Other f <*> Pure x = Other (($ x) <$> f)
    Other f <*> Other y = Other (f <*> y)
Run Code Online (Sandbox Code Playgroud)

现在,如果我们添加Constant函子/applicative:

newtype Constant a b = Constant { getConstant :: a }

instance Functor (Constant a) where
    fmap f (Constant x) = Constant x

instance (Monoid a) => Applicative (Constant a) where
    pure _ = Constant mempty
    Constant x <*> Constant y = Constant (x `mappend` y)
Run Code Online (Sandbox Code Playgroud)

...我们可以从和Either的其他响应中组装出“适用性” :LiftConstant

type Error e a = Lift (Constant e) a
Run Code Online (Sandbox Code Playgroud)