Monad对申请人有什么好处?

arr*_*owd 35 monads haskell functional-programming applicative

我已经阅读了这篇文章,但最后一节并未理解.

作者说Monad给了我们上下文敏感性,但是只使用Applicative实例就可以实现相同的结果:

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

没有do语法肯定会更加丑陋,但除此之外我不明白为什么我们需要Monad.任何人都可以为我清除这个吗?

Car*_*arl 58

这里有几个使用该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)
Run Code Online (Sandbox Code Playgroud)

您无法使用该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
Run Code Online (Sandbox Code Playgroud)

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

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

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

如果你把它分解为考虑形式的值f a是关于"效果"和"结果"(两者都是非常模糊的近似术语,这是最好的术语,但不是很好),你可以在这里提高你的理解.在类型值的情况下Maybe a,"效果"是成功或失败,作为计算."result"是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)
Run Code Online (Sandbox Code Playgroud)

嗯..发生的是编译错误.(<*>)那里没有合适的类型.whileA p step有类型a -> f astep x类型f a.(<*>)是不是适合他们的形状.要使它工作,需要函数类型f (a -> a).

你可以尝试更多的东西 - 但是你最终会发现whileA没有任何实现可以运行甚至接近的方式whileM.我的意思是,你可以实现类型,但是没有办法使它既循环又终止.

使其工作要求任何join(>>=).(好吧,或其中之一的其中一个)和那些你从Monad界面中得到的额外的东西.

  • 的确。很好的解释:_Applicative 接口允许“结果”控制流是动态的,但它要求“效果”控制流是静态的。_ 这是 SO 上的伟大句子之一。 (2认同)

Pet*_*lák 24

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

main = do
    b <- readLn :: IO Bool
    if b
      then fireMissiles
      else return ()
Run Code Online (Sandbox Code Playgroud)

你不能用Applicatives 做- 一个有效计算的结果值不能确定将遵循什么效果.

有点相关:

  • @arrowdodger不,如果`fireMissilies`类型为`IO()`那么你就不能..你只能使用纯函数`<$>`,而不能使用有效函数. (15认同)
  • @arrowdodger要添加到Petr所说的内容:如果你使用`(\ b - >如果b然后是fireMissiles,则返回())<$> readLn`,你会得到类型为"IO(IO())"的东西 - 要回到`IO()`,你需要`join :: Monad m => m(ma) - > ma`. (14认同)

Ant*_*sky 23

正如Stephen Tetley在评论中所说,该示例实际上并不使用上下文敏感性.考虑上下文敏感性的一种方法是,它允许根据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
Run Code Online (Sandbox Code Playgroud)

让我们将其加载到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!
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.但是如果我们使用适用版本:

*Main> checkPasswordA
What's the password?
hunter2
Correct.  The secret answer is 42.
INTRUDER ALERT!  INTRUDER ALERT!
Run Code Online (Sandbox Code Playgroud)

我们输入了错误的密码,但我们仍然得到了秘密! 入侵者警报!这是因为,<$><*>,或等价/ ,始终执行的影响,所有的参数.应用版本用符号表示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
Run Code Online (Sandbox Code Playgroud)

应该清楚为什么这有错误的行为.实际上,每个使用applicative functor都等同于表单的monadic代码

do val1 <- app1
   val2 <- app2
   ...
   valN <- appN
   pure $ f val1 val2 ... valN
Run Code Online (Sandbox Code Playgroud)

(其中一些appI被允许的形式pure xI).并且等效地,该形式的任何monadic代码都可以被重写为

f <$> app1 <*> app2 <*> ... <*> appN
Run Code Online (Sandbox Code Playgroud)

或者相当于

liftAN f app1 app2 ... appN
Run Code Online (Sandbox Code Playgroud)

考虑一下,考虑一下Applicative方法:

pure  :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

然后考虑Monad添加的内容:

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

(请记住,你只需要其中一个.)

如果你仔细想想,我们可以把应用函数放在一起的唯一方法就是构造链形式f <$> app1 <*> ... <*> appN,并且可能嵌套这些链(例如,f <$> (g <$> x <*> y) <*> z).但是,(=<<)(或(>>=))允许我们取一个值并根据该值生成不同的 monadic计算,这些计算可以在运行中构建.这就是我们用来决定是否计算"打印秘密",或者计算"打印出入侵者警报",以及为什么我们不能单独使用应用函子来做出决定; 应用函数的任何类型都不允许您使用普通值.

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

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

checkPasswordM' :: IO ()
checkPasswordM' = join checkPasswordA'
Run Code Online (Sandbox Code Playgroud)

这与以前的monadic版本完全相同(只要我们import Control.Monad第一次获得join):

*Main> checkPasswordM'
What's the password?
12345
INTRUDER ALERT!  INTRUDER ALERT!
Run Code Online (Sandbox Code Playgroud)

  • 可惜我不能接受一个以上的答案! (2认同)

J. *_*son 10

另一方面,这里是Applicative/ Monaddivide Applicative的一个实际例子,其中s有一个优点:错误处理!我们显然有Monad实施Either沿着错误进行,但它总是早早结束.

Left e1 >> Left e2    ===   Left e1
Run Code Online (Sandbox Code Playgroud)

您可以将此视为混合值和上下文的效果.因为(>>=)将尝试将Either e a值的结果传递给类似的函数a -> Either e b,所以如果输入是,它必须立即失败.EitherLeft

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

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

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

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

> Error "a" <*> Error "b"
Error "ab"
Run Code Online (Sandbox Code Playgroud)


Mat*_*hid 7

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

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

正如您所看到的,有时候Applicative的"限制​​"实际上是有益的 - 有时Monad提供的额外功能是完成工作所必需的.这就是我们两者兼得的原因.

  • 这个答案和电话为Monad与Applicative问题提供了非常有价值的反面:Monads可以表达Applicatives不能的流量控制,但另一方面,Applicative可以支持强大的**静态分析**,Monad通常是不可能的.[Capriotti&Kaposi最近的论文](http://paolocapriotti.com/blog/2013/04/03/free-applicative-functors/)是我最喜欢的这个主题,不过我还会提到[我的操作应用库] (https://github.com/sacundim/free-operational); 在页面的README上有一个解析器优化示例. (2认同)

Ank*_*kur 5

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

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

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

现在您可以从上面的描述中看到,bind与之相比,您可以获得更多控制权<*>


Lan*_*dei 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].