【问题标题】:What advantage does Monad give us over an Applicative?与 Applicative 相比,Monad 给了我们什么优势?
【发布时间】:2013-06-28 20:33:54
【问题描述】:

我读过this article,但没看懂最后一节。

作者说 Monad 为我们提供了上下文敏感性,但仅使用 Applicative 实例也可以达到相同的结果:

let maybeAge = (\futureYear birthYear -> if futureYear < birthYear
    then yearDiff birthYear futureYear
    else yearDiff futureYear birthYear) <$> (readMay futureYearString) <*> (readMay birthYearString)

如果没有 do 语法肯定会更丑,但除此之外,我不明白为什么我们需要 Monad。谁能帮我解决这个问题?

【问题讨论】:

  • Monads 确实为我们提供了上下文敏感性,但本文并未真正使用它——它总是执行两次解析,然后(可能)根据它们的值交换它们。显示上下文敏感性的示例可能决定不解析第二个值,具体取决于第一个值。
  • 在这种情况下不会因为懒惰而无法解析第二个值吗?
  • 好问题,我的假设是懒惰并不重要,两个动作都被执行(理由 - 请参阅原始应用论文 soi.city.ac.uk/~ross/papers/Applicative.pdf 中的 iffy),但我可能是错的。跨度>

标签: haskell functional-programming monads applicative


【解决方案1】:

这里有几个使用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)

您不能使用Applicative 接口来实现它们。但是为了启蒙,让我们试着看看哪里出了问题。怎么样..

import Control.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

看起来不错!它有正确的类型,它必须是相同的东西!让我们检查以确保..

*Main> ifM (Just True) (Just 1) (Just 2)
Just 1
*Main> ifM (Just True) (Just 1) (Nothing)
Just 1
*Main> ifA (Just True) (Just 1) (Just 2)
Just 1
*Main> ifA (Just True) (Just 1) (Nothing)
Nothing

这是您对差异的第一个暗示。您不能只使用复制ifMApplicative 接口来编写函数。

如果您将其划分为将f a 形式的值视为“效果”和“结果”(两者都是非常模糊的近似术语,是可用的最佳术语,但不是很好),你可以在这里提高你的理解。对于Maybe a 类型的值,“效果”是成功或失败,作为计算。 “结果”是一个 a 类型的值,在计算完成时可能会出现。 (这些术语的含义很大程度上取决于具体类型,所以不要认为这是对 Maybe 以外的任何类型的有效描述。)

鉴于该设置,我们可以更深入地了解差异。 Applicative 接口允许“结果”控制流是动态的,但它要求“效果”控制流是静态的。如果您的表达式涉及 3 个可能失败的计算,则其中任何一个的失败都会导致整个计算失败。 Monad 接口更加灵活。它允许“效果”控制流依赖于“结果”值。 ifM 根据第一个参数选择要包含在其自己的“效果”中的参数的“效果”。这是ifAifM 之间巨大的根本区别。

whileM 发生了更严重的事情。让我们尝试制作whileA,看看会发生什么。

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)

嗯.. 发生的是编译错误。 (&lt;*&gt;) 那里没有正确的类型。 whileA p step 的类型为 a -&gt; f astep x 的类型为 f a(&lt;*&gt;) 不是将它们组合在一起的正确形状。要让它工作,函数类型需要是f (a -&gt; a)

你可以尝试更多的东西——但你最终会发现whileA 没有任何实现方式可以像whileM 那样工作。我的意思是,你可以实现这个类型,但是没有办法让它既循环又终止。

使其工作需要join(&gt;&gt;=)。 (嗯,或者其中之一的许多等价物之一)还有那些你从Monad 界面中得到的额外东西。

【讨论】:

  • 区分“结果”和“效果”是关键。非常感谢这么长的帖子。
  • 确实如此。很好的解释:Applicative 接口允许“结果”控制流是动态的,但它要求“效果”控制流是静态的。 这是关于 SO 的伟大句子之一。
  • 我读过在 Monads 和 Applicatives 之间有一个是超级集合(或比另一个更强大),但我一直很困惑。这篇文章把事情说得很清楚。谢谢。
【解决方案2】:

使用 monad,后续效果可能取决于之前的值。例如,您可以:

main = do
    b <- readLn :: IO Bool
    if b
      then fireMissiles
      else return ()

Applicatives 无法做到这一点 - 一次有效计算的结果值无法确定接下来会发生什么效果。

有点相关:

【讨论】:

  • 嗯。我可以(\b -&gt; if b then fire else ()) &lt;$&gt; readLn,不是吗?
  • @arrowdodger 不,如果fireMissiliesIO () 类型,那么你不能......你只能将&lt;$&gt; 用于纯函数,而不是有效函数。
  • @arrowdodger 补充一下 Petr 所说的:如果你确实使用了(\b -&gt; if b then fireMissiles else return ()) &lt;$&gt; readLn,你会得到IO (IO ()) 类型的东西——要回到IO (),你需要@987654332 @.
【解决方案3】:

As Stephen Tetley said in a comment,该示例实际上并未使用上下文相关性。考虑上下文敏感性的一种方法是,它允许使用根据单子值选择要采取的操作。在某种意义上,应用计算必须始终具有相同的“形状”,而不管所涉及的值如何;一元计算不需要。我个人认为这通过一个具体的例子更容易理解,所以让我们看一个。这是一个简单程序的两个版本,要求您输入密码,检查您输入的密码是否正确,然后根据您是否输入密码打印出响应。

import Control.Applicative

checkPasswordM :: IO ()
checkPasswordM = do putStrLn "What's the password?"
                    pass <- getLine
                    if pass == "swordfish"
                      then putStrLn "Correct.  The secret answer is 42."
                      else putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

checkPasswordA :: IO ()
checkPasswordA =   if' . (== "swordfish")
               <$> (putStrLn "What's the password?" *> getLine)
               <*> putStrLn "Correct.  The secret answer is 42."
               <*> putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

if' :: Bool -> a -> a -> a
if' True  t _ = t
if' False _ f = f

让我们将它加载到 GHCi 并检查 monadic 版本会发生什么:

*Main> checkPasswordM
What's the password?
swordfish
Correct.  The secret answer is 42.
*Main> checkPasswordM
What's the password?
zvbxrpl
INTRUDER ALERT!  INTRUDER ALERT!

到目前为止,一切都很好。但是如果我们使用应用版本:

*Main> checkPasswordA
What's the password?
hunter2
Correct.  The secret answer is 42.
INTRUDER ALERT!  INTRUDER ALERT!

我们输入了错误的密码,但我们仍然得到了秘密! 并且一个入侵者警报!这是因为&lt;$&gt;&lt;*&gt;,或者等价的liftA<em>n</em>/liftM<em>n</em>总是执行所有它们的参数的效果。应用程序版本以do 表示法转换为

do pass  <- putStrLn "What's the password?" *> getLine)
   unit1 <- putStrLn "Correct.  The secret answer is 42."
   unit2 <- putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"
   pure $ if' (pass == "swordfish") unit1 unit2

并且应该清楚为什么会有错误的行为。事实上,每一次应用函子的使用都等价于形式的一元代码

do val1 <- app1
   val2 <- app2
   ...
   valN <- appN
   pure $ f val1 val2 ... valN

(其中一些appI 允许采用pure xI 的形式)。等效地,该形式的任何一元代码都可以重写为

f <$> app1 <*> app2 <*> ... <*> appN

或等价于

liftAN f app1 app2 ... appN

要考虑这一点,请考虑Applicative 的方法:

pure  :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b

然后考虑Monad 添加的内容:

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

(请记住,您只需要其中一个。)

手忙脚乱,如果您考虑一下,我们可以将应用函数组合在一起的唯一方法是构造 f &lt;$&gt; app1 &lt;*&gt; ... &lt;*&gt; appN 形式的链,并可能嵌套这些链(例如,@ 987654342@)。但是,(=&lt;&lt;)(或(&gt;&gt;=))允许我们获取一个值并根据该值生成不同的单子计算,这些计算可以动态构建。这就是我们用来决定是计算“打印出秘密”还是计算“打印出入侵者警报”,以及为什么我们不能单独使用应用函子做出决定的原因;应用函数的任何类型都不允许您使用普通值。

你可以用类似的方式考虑joinfmapas I mentioned in a comment,你可以做类似的事情

checkPasswordFn :: String -> IO ()
checkPasswordFn pass = if pass == "swordfish"
                         then putStrLn "Correct.  The secret answer is 42."
                         else putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

checkPasswordA' :: IO (IO ())
checkPasswordA' = checkPasswordFn <$> (putStrLn "What's the password?" *> getLine)

当我们想要根据值选择不同的计算时会发生这种情况,但我们只能使用应用程序功能。我们可以选择两个不同的计算来返回,但它们被包裹在应用函子的外层中。要实际使用我们选择的计算,我们需要join

checkPasswordM' :: IO ()
checkPasswordM' = join checkPasswordA'

这和之前的monadic版本做的事情是一样的(只要我们先import Control.Monad,得到join):

*Main> checkPasswordM'
What's the password?
12345
INTRUDER ALERT!  INTRUDER ALERT!

【讨论】:

    【解决方案4】:

    另一方面,这里有一个Applicative/Monad 除法的实际示例,其中Applicatives 有一个优势:错误处理!我们显然有一个 EitherMonad 实现,它带有错误,但它总是提前终止。

    Left e1 >> Left e2    ===   Left e1
    

    您可以将其视为混合值和上下文的效果。由于(&gt;&gt;=) 将尝试将Either e a 值的结果传递给a -&gt; Either e b 之类的函数,如果输入EitherLeft,它必须立即失败。

    Applicatives 仅在运行所有效果后将它们的值传递给最终的纯计算。这意味着他们可以延迟访问这些值的时间更长,我们可以这样写。

    data AllErrors e a = Error e | Pure a deriving (Functor)
    
    instance Monoid e => Applicative (AllErrors e) where
      pure = Pure
      (Pure f) <*> (Pure x) = Pure (f x)
      (Error e) <*> (Pure _) = Error e
      (Pure _) <*> (Error e) = Error e
      -- This is the non-Monadic case
      (Error e1) <*> (Error e2) = Error (e1 <> e2)
    

    不可能为AllErrors 编写一个Monad 实例以使ap(&lt;*&gt;) 匹配,因为(&lt;*&gt;) 利用了在使用任何值之前运行第一个和第二个上下文的优势为了将两个错误和(&lt;&gt;) 放在一起。 Monadic (&gt;&gt;=)(join) 只能访问与其值交织在一起的上下文。这就是为什么EitherApplicative实例是左偏的,这样它也可以有一个和谐的Monad实例。

    > Left "a" <*> Left "b"
    Left 'a'
    
    > Error "a" <*> Error "b"
    Error "ab"
    

    【讨论】:

    • 嘿,谢谢。这并不是对所提问题的特别回答,但它对昨晚有人在 IRC 中提出的关于可以收集所有错误的 Either 应用程序的问题的回答。我希望我在那之前读过这个答案。但即使我不能回答这个问题,我也学到了一些新东西。
    • 我想发布它,尽管有点离题,因为对我来说,这是MonadApplicative 之间值/上下文时间区别的最清晰、最简单的示例之一。无论我是在构建 Monadic 还是 Applicative 界面,这都是我的首选思维模型。
    • 我也喜欢它,因为它是解析器何时能够收集多个错误的一个很好的例子——如果它们是上下文敏感的,它们需要一个 Monad,然后必须对效果进行排序。实际上,这是同一件事的两种说法。
    【解决方案5】:

    使用 Applicative,要执行的有效操作的顺序在编译时是固定的。使用 Monad,它可以在运行时根据效果的结果而变化。

    例如,对于 Applicative 解析器,解析操作的顺序始终是固定的。这意味着您可以对其执行“优化”。另一方面,我可以编写一个 Monadic 解析器,它解析一些 BNF 语法描述,动态地为该语法构造一个解析器,然后在其余输入上运行该解析器。每次运行此解析器时,它都可能构建一个全新的解析器来解析输入的第二部分。 Applicative 没有希望做这样的事情 - 并且没有机会对尚不存在的解析器执行编译时优化......

    如您所见,有时 Applicative 的“限制”实际上是有益的 - 有时需要 Monad 提供的额外功能才能完成工作。这就是我们两者兼有的原因。

    【讨论】:

    • 这个答案和电话为 Monad vs. Applicative 问题提供了一个非常有价值的另一面:Monads 可以表达 Applicatives 不能表达的流量控制,但另一方面,Applicative 可以支持强大的静态分析 这对 Monad 来说通常是不可能的。 Capriotti & Kaposi's recent paper 是我在这个话题上最喜欢的,不过我也会提到my operational Applicative library;在页面的 README 中有一个解析器优化示例。
    【解决方案6】:

    如果你尝试将 Monad 的 bind 和 Applicative &lt;*&gt; 的类型签名转换为自然语言,你会发现:

    bind : 会给你包含的价值,会给我一个新的包装价值

    &lt;*&gt;给我一个封装函数,它接受一个包含的值并返回一个值,将使用它根据我的规则创建新的封装值.

    现在从上面的描述可以看出,bind&lt;*&gt; 给你更多的控制权

    【讨论】:

      【解决方案7】:

      如果您使用 Applicatives,则结果的“形状”已经由输入的“形状”确定,例如如果您调用[f,g,h] &lt;*&gt; [a,b,c,d,e],您的结果将是一个包含 15 个元素的列表,无论变量具有哪些值。 monads 没有这个保证/限制。考虑[x,y,z] &gt;&gt;= join replicate:对于[0,0,0],您将获得结果[],对于[1,2,3],您将获得结果[1,2,2,3,3,3]

      【讨论】:

        【解决方案8】:

        现在ApplicativeDo 扩展名已经很普遍了,MonadApplicative 之间的区别可以用简单的代码 sn-p 来说明。

        Monad 你可以做到

        do
           r1 <- act1
           if r1
                then act2
                else act3
        

        但只有Applicative do-block,你不能在你用&lt;-拉出的东西上使用if

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-07-04
          • 2011-03-13
          • 1970-01-01
          • 1970-01-01
          • 2011-09-27
          • 2016-07-03
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多