Haskell 中 Monad 和 Applicative 之间的区别

Difference between Monad and Applicative in Haskell

提问人:thor 提问时间:4/28/2014 更新时间:12/20/2022 访问量:18945

问:

我刚刚从 typeclassopedia 上阅读了以下关于 和 之间的区别的内容。我能理解没有.但下面的描述对我来说似乎很模糊,我无法弄清楚一元计算/操作的“结果”到底是什么意思。那么,如果我把一个值放进去,形成一个单子,这个“计算”的结果是什么?MonadApplicativejoinApplicativeMaybe

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

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

Haskell Monads 应用型

评论

7赞 Will Ness 4/28/2014
Just 1描述一个“计算”,其“结果”为 1。 描述不产生任何结果的计算。Nothing
3赞 Antal Spector-Zabusky 4/29/2014
另见arrowdodger的问题“Monad给了我们什么优势?”,它有一些很好的答案(完全披露:包括我的一个)。

答:

83赞 J. Abrahamson 4/28/2014 #1

我最喜欢的例子是“纯粹适用的 Either”。我们将首先分析 Either 的基本 Monad 实例

instance Monad (Either e) where
  return = Right
  Left e  >>= _ = Left e
  Right a >>= f = f a

这个实例嵌入了一个非常自然的短路概念:我们从左到右进行,一旦单个计算“失败”,那么所有其他计算也会失败。还有一种自然的例子,任何人都有LeftApplicativeMonad

instance Applicative (Either e) where
  pure  = return
  (<*>) = ap

其中无非是 a 之前的从左到右排序:apreturn

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

如果我们认为是“过去”和“未来”,那么只要它能够运行“步进器”,就会从过去产生未来。这个“步进器”要求未来真正存在的价值......这是不可能的.因此需要短路。m am b(>>=)(a -> m b)aEither(>>=)

因此,我们将实现一个不能有相应 .ApplicativeMonad

instance Monoid e => Applicative (Either e) where
  pure = Right

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

  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!

再次注意,如果我们把左边的论点看作是“过去”,把右边的论点看作是“未来”,那么与它相比,它是特别的,因为它被允许并行地“开放”未来和过去,而不一定需要“过去”的结果来计算“未来”。(<*>)(>>=)

这直接意味着,我们可以使用 our pure 来收集错误,如果链中存在任何 s,则忽略 sApplicativeEitherRightLeft

> 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

评论

4赞 Will Ness 4/29/2014
怎么了?ifA t c a = g <$> t <*> c <*> a where g b x y = if b then x else y
14赞 Antal Spector-Zabusky 4/29/2014
@WillNess:它总是使用所有的计算结构/运行所有的效果。例如,而 .说“我们不能用预期的语义写作”可能更准确。ifA (Just True) (Just ()) Nothing == NothingifM (Just True) (Just ()) Nothing == Just ()ifA
1赞 J. Abrahamson 4/29/2014
我认为这正是检查单子力量的工具,尽管它确实假设这是预期的语义而不是。真正的挑战是当效果与价值观混为一谈时。ifMifMif' <$> a <*> b <*> c where if' b t e = if b then t else e
8赞 Luis Casillas 4/30/2014
我认为重要的是要注意,当一个类型定义了一个实例时,它的实例必须与该实例兼容 (, (<*>) = apApplicative' 本答案中的实例定义满足法律,它违反了这个记录的要求。获取第二个实例的正确方法是为同构于 .MonadApplicativeMonadpure = return). While the second ApplicativeApplicativeEither
2赞 Luis Casillas 4/30/2014
另请注意,Control.Applicative.Lift 中的类型精确地实现了此答案中描述的“收集所有错误”行为。Errors
18赞 Sassa NF 4/28/2014 #2

差异的关键可以在 的类型和 的类型中观察到。ap=<<

ap :: m (a -> b) -> (m a -> m b)
=<< :: (a -> m b) -> (m a -> m b)

在这两种情况下都有 ,但只有在第二种情况下才能决定是否应用该函数。反过来,函数可以“决定”是否应用下一个绑定的函数 - 通过生成不“包含”(如 或 )的函数。m am a(a -> m b)(a -> m b)m bb[]NothingLeft

“内部”函数无法做出这样的“决策”——它们总是产生一个类型的值。Applicativem (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.

这是不可能的,所以不能举例说明。最接近的是: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
53赞 Will Ness 4/29/2014 #3

Just 1描述一个“计算”,其“结果”为 1。 描述不产生任何结果的计算。Nothing

单子和应用之间的区别在于,在单子中有一个选择。Monads的主要区别在于能够在不同的计算路径之间进行选择(而不仅仅是提前突破)。根据计算中前一步生成的值,计算结构的其余部分可能会发生变化。

这就是这意味着什么。在单子链中

return 42            >>= (\x ->
if x == 1
   then
        return (x+1) 
   else 
        return (x-1) >>= (\y -> 
        return (1/y)     ))

选择要构建的计算。if

在应用的情况下,在

pure (1/) <*> ( pure (+(-1)) <*> pure 1 )

所有函数都在“内部”计算中工作,没有机会打破链条。每个函数只是转换它馈送的一个值。从函数的角度来看,计算结构的“形状”完全是“外部”的。

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

对于单子,函数本身会根据自己的选择构建计算。

评论

5赞 eazar001 9/22/2014
这个例子简明扼要地演示了几件事:你不仅可以像使用应用函子一样转换值,还可以......1)将计算历史存储在一元运算链中的任何位置,2)根据保存的计算历史决定如何以及何时转换值(如果要转换值)(以可能的非线性方式),3)在这些一元运算的主体中模拟副作用,4)更琐碎的是,使用符号。do-block
1赞 Will Ness 1/22/2016
参见后来我的这个相关的答案,以进行一些澄清比较。
1赞 Ivan 1/17/2018
标记为正确的那个是无用的,而这个 unswer 真的很有帮助,尤其是当你不熟悉该语言时......
3赞 Will Ness 1/17/2018
@Ivan对于非 Haskellers 来说可能更难,但实际上要好得多。关键的区别在于,应用组合 ( ) 中涉及的所有计算描述都是预先知道的;但是使用一元组合 ( ),每个下一个计算都根据前一个计算产生的值来计算(“依赖于”)。这涉及到两个时间线,两个世界:一个是创建和组合计算描述(,...)的纯粹世界,另一个是“运行”它们的潜在不纯世界 - 实际计算发生的地方。a <*> b <*> ...a >>= (\ ... -> b >>= ... )ab
2赞 Will Ness 11/7/2018
这个答案就是一个很好的例子。
19赞 thor 4/29/2014 #4

以下是我对亚伯拉罕森@J的例子的看法,说明为什么不能使用里面的值,例如。从本质上讲,它仍然归结为 中没有函数 ,它统一了 typeclassopedia 中给出的两种不同观点来解释 和 之间的区别。ifA(pure True)joinMonadApplicativeMonadApplicative

因此,使用亚伯拉罕森@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!

(具有与 相似的短路效应),以及函数EitherMonadifA

ifA :: Applicative f => f Bool -> f a -> f a -> f a

如果我们尝试实现上述方程会怎样:

ifA (pure True)  t e == t
ifA (pure False) t e == e

?

好吧,正如已经指出的,最终,的内容不能被后面的计算使用。但从技术上讲,这是不对的。我们可以使用 因为 a 也是 a 的 content with .我们可以做到:(pure True)(pure True)MonadFunctorfmap

ifA' b t e = fmap (\x -> if x then t else e) b

问题出在 的返回类型上,即 。在 中,无法将两个嵌套的 S 折叠为一个。但这种坍塌功能恰恰是 in 所执行的。所以ifA'f (f a)ApplicativeApplicativejoinMonad

ifA = join . ifA' 

将满足 的方程,如果我们能适当地实现。这里缺少的正是功能。换句话说,我们可以以某种方式将前一个结果的结果用于 。但是在框架中这样做将涉及将返回值的类型增加到嵌套的应用值,我们没有办法将其恢复到单级应用值。这将是一个严重的问题,因为,例如,我们无法适当地使用 S 来组合函数。使用解决了这个问题,但引入 将 提升到 .ifAjoinApplicativejoinApplicativeApplicativeApplicativejoinjoinApplicativeMonad

评论

1赞 RomnieEE 8/31/2017
这里加入的问题是否是这 3 个可以的:但这不好:?join Right (Right a) = Right a; join Right (Left e) = Left e; join Left (Left e) = Left ejoin Left (Right a) =? Left (Right a)
0赞 RomnieEE 8/31/2017
实际上,第一次尝试一下,我发现我最终陷入了一团糟,因为 的参数 ,或者更糟。joinResult<Result<_,_>,Result<_,_>>
6赞 Luis Casillas 4/30/2014 #5

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

嗯,这种模糊性有点故意的,因为一元计算的“结果”取决于每种类型。最好的答案有点自相矛盾:“结果”(或结果,因为可以有多个)是实例的实现调用函数参数的任何值。(>>=) :: 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

这里唯一符合“结果”条件的东西是 的第二个等式中的 ,因为它是唯一被“喂”给 的第二个参数的东西。a>>=>>=

其他答案已经深入探讨了 vs. 差异,所以我想我应该强调另一个显着的区别:应用者组成,单子不。使用 s,如果你想制作一个结合了两个现有效果的效果,你必须将其中一个重写为 monad 转换器。相反,如果你有两个,你可以很容易地用它们制作一个更复杂的,如下所示。(代码是从转换器复制粘贴的。ifAifMMonadMonadApplicatives

-- | 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)

现在,如果我们添加 functor/appplicative:Constant

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)

...我们可以从其他响应中组装出“应用”和:EitherLiftConstant

type Error e a = Lift (Constant e) a
1赞 user3680029 9/21/2018 #6

我想分享我对这个“iffy miffy”的看法,因为我理解上下文中的所有内容都会得到应用,例如:

iffy :: Applicative f => f Bool -> f a -> f a -> f a
iffy fb ft fe = cond <$> fb <*> ft <*> fe   where
            cond b t e = if b then t else e

case 1>> iffy (Just True) (Just “True”) Nothing ->> Nothing

upps 应该只是“真实”......但

 case 2>> iffy (Just False) (Just “True”) (Just "False") ->> Just "False" 

(“好”的选择是在上下文中做出的) 我以这种方式向自己解释了这一点,就在计算结束之前,以防万一 >>1 我们在“链”中得到类似的东西:

Just (Cond True "True") <*> something [something being "accidentaly" Nothing]

根据 Applicative 的定义,其评估为:

fmap (Cond True "True") something 

当“某物” Nothing 时,根据 Functor 约束,它变成 Nothing(fmap over Nothing gives Nothing)。并且不可能用故事结尾的“fmap f Nothing = something”来定义 Functor。

2赞 michid 10/28/2021 #7

正如 @Will Ness 在他的回答中解释的那样,关键的区别在于,对于 Monads,每一步都可以在不同的执行路径之间进行选择。让我们通过实现一个四次排序的函数,使这个潜在的选择在语法上可见。首先是应用,然后是单子:fm

seq4A :: Applicative f => f a -> f [a]
seq4A f =
    f <**> (
    f <**> (
    f <**> (
    f <&> (\a1 a2 a3 a4 -> 
        [a1, a2, a3, a4]))))

seq4M :: Monad m => m a -> m [a]
seq4M m =
    m >>= (\a1 ->
    m >>= (\a2 ->
    m >>= (\a3 ->
    m >>= (\a4 -> 
        return [a1, a2, a3, a4]))))

该函数具有由每一步可用的单子操作生成的值,因此可以在每一步做出选择。另一方面,该函数只有最后可用的值。seq4Mseq4A