40

我读过这篇文章,但没有理解最后一节。

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

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

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

4

8 回答 8

64

这是使用该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

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

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

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

还有更严重的事情正在发生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)

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

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

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

于 2013-07-01T20:20:54.897 回答
26

对于 monad,后续效果可能取决于先前的值。例如,您可以拥有:

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

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

有点相关:

于 2013-07-01T16:44:05.657 回答
25

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

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!

我们输入了错误的密码,但我们仍然得到了秘密! 还有入侵者警报!这是因为<$>and<*>或等效的/总是执行所有参数的效果。应用版本在符号上翻译为liftAnliftMndo

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's 方法:

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 <$> app1 <*> ... <*> appN,并可能嵌套这些链(例如f <$> (g <$> x <*> y) <*> z)。但是,(=<<)(或(>>=))允许我们取一个值并根据该值产生不同的一元计算,这些计算可以即时构建。这就是我们用来决定是计算“打印出秘密”还是计算“打印出入侵者警报”,以及为什么我们不能单独使用应用函子做出决定的原因;应用函数的任何类型都不允许您使用普通值。

你可以joinfmap类似的方式来考虑:正如我在评论中提到的,你可以做类似的事情

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!
于 2013-07-01T19:42:34.717 回答
11

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

Left e1 >> Left e2    ===   Left e1

您可以将其视为混合值和上下文的效果。由于(>>=)将尝试将Either e a值的结果传递给类似的函数,因此如果输入为a -> 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)

不可能为这样的匹配编写一个Monad实例,因为在使用任何值之前利用运行第一个和第二个上下文来将两个错误和它们放在一起。ic并且只能访问与其值交织在一起的上下文。这就是为什么的实例是左偏的,这样它也可以有一个和谐的实例。AllErrorsap(<*>)(<*>)(<>)Monad(>>=)(join)EitherApplicativeMonad

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

> Error "a" <*> Error "b"
Error "ab"
于 2013-07-02T15:21:40.330 回答
7

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

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

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

于 2013-07-02T07:45:21.427 回答
5

如果您使用 Applicatives,则结果的“形状”已经由输入的“形状”确定,例如,如果您调用[f,g,h] <*> [a,b,c,d,e],您的结果将是 15 个元素的列表,而不管变量具有哪些值。monads 没有这个保证/限制。考虑[x,y,z] >>= join replicate:为了[0,0,0]你会得到结果[],为了[1,2,3]结果[1,2,2,3,3,3]

于 2013-07-02T07:37:36.233 回答
5

如果你尝试将 Monadbind和 Applicative的类型签名转换<*>为自然语言,你会发现:

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

<*>:给我一个打包函数,它接受一个包含的值并返回一个值,将使用它根据我的规则创建新的打包值。

现在从上面的描述中可以看出,bind<*>

于 2013-07-02T05:03:30.447 回答
1

现在扩展变得非常普遍,可以使用简单的代码片段来说明和ApplicativeDo之间的区别。MonadApplicative

Monad你可以做

do
   r1 <- act1
   if r1
        then act2
        else act3

但是只有Applicativedo-block,你不能if在你用<-.

于 2019-07-20T15:18:57.733 回答