Monad对申请人有什么好处?

时间:2013-07-01 16:26:15

标签: haskell functional-programming monads applicative

我已阅读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。任何人都可以为我解决这个问题吗?

8 个答案:

答案 0 :(得分:55)

以下是使用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的{​​{1}}接口来编写函数。

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

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

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 stepa -> f a类型为step xf a不适合将它们组合在一起。要使其工作,函数类型必须为(<*>)

你可以尝试更多的东西 - 但是你最终会发现f (a -> a)没有任何实现可以使用whileA的方式。我的意思是,你可以实现类型,但是没有办法使它既循环又终止。

使其工作需要 whileMjoin。 (好吧,或其中之一的其中一个)和那些你从(>>=)界面得到的额外的东西。

答案 1 :(得分:24)

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

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

你不能用Applicative s做到这一点 - 一次有效计算的结果值无法确定后面会有什么效果。

有点相关:

答案 2 :(得分:21)

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

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!

我们输入了错误的密码,但我们仍然得到了秘密! 入侵警报!这是因为<$><*>或等效liftAn / liftMn总是执行所有的影响参数。应用版本以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

应该清楚为什么这有错误的行为。事实上,每个使用的applicative functor都相当于形式的monadic代码

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

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

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

您可以以类似的方式考虑与join一致的fmapas 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)

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

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

这与之前的monadic版本完全相同(只要我们import Control.Monad首先获得join):

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

答案 3 :(得分:10)

另一方面,这是Applicative / Monad划分的一个实际示例,其中Applicative具有优势:错误处理!我们显然Monad Either的{​​{1}}实现会带来错误,但它总是会提前终止。

Left e1 >> Left e2    ===   Left e1

您可以将此视为混合价值观和背景的效果。由于(>>=)会尝试将Either e a值的结果传递给类似a -> Either e b的函数,因此如果输入Either为<{1}},则必须立即失败Left

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

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编写AllErrors个实例是不可能的,ap匹配(<*>),因为(<*>)利用了第一个和第二个上下文的运行< em>在使用任何值之前以便同时获取错误和(<>)Monad ic (>>=)(join)只能访问与其值相互交织的上下文。这就是为什么Either的{​​{1}}实例偏向左侧,因此它也可以有一个和谐的Applicative实例。

Monad

答案 4 :(得分:7)

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

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

正如您所看到的,有时候Applicative的“限制​​”实际上是有益的 - 有时Monad提供的额外功能是完成工作所必需的。这就是为什么我们都有这两个原因。

答案 5 :(得分:5)

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

bind会为您提供所包含的值,会返回一个新的打包值

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

现在您可以从上面的说明中看到,与bind

相比,<*>为您提供了更多控制权

答案 6 :(得分:5)

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

答案 7 :(得分:1)

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

使用Monad,您可以完成

do
   r1 <- act1
   if r1
        then act2
        else act3

但是只有Applicative个阻塞,您不能对用if拔出的东西使用<-