如何使用Control.Applicative编写更清晰的Haskell代码?

61
在最近的 关于样式问题的回答 中,我写道
main = untilM (isCorrect 42) (read `liftM` getLine)

并且

isCorrect num guess =
  case compare num guess of
    EQ -> putStrLn "You Win!" >> return True
    ...

Martijn乐于提供替代方案:

main = untilM (isCorrect 42) (read <$> getLine)

EQ -> True <$ putStrLn "You Win!"

使用Control.Applicative的抽象可以使Haskell代码中哪些常见模式更清晰?使用Control.Applicative有效的有哪些有用的经验法则?

3个回答

48

对于你的问题,有许多可以回答的内容。但是,既然你问了,我就提供一个“经验法则”。

如果你正在使用do表示法,并且你生成的值[1]没有用于你正在顺序执行的表达式[2]中,那么此代码可以转换为Applicative风格。类似地,如果你在一个被顺序执行的表达式中使用了一个或多个生成的值,则必须使用Monad,而Applicative不能够达到相同的效果。

例如,让我们看以下代码:

do a <- e1
   b <- e2
   c <- e3
   return (f a b c)

我们看到在<-右侧的任何表达式中,都没有出现生成的值(abc)。因此,我们可以将其转换为使用Applicative代码。以下是一种可能的转换方式:

f <$> e1 <*> e2 <*> e3

还有一个:

liftA3 f e1 e2 e3

另一方面,以这段代码为例:

do a <- e1
   b <- e2 a
   c <- e3
   return (f b c)

这段代码不能使用Applicative[3],因为生成的值a在推导式中稍后用于一个表达式。必须使用Monad才能得到结果--尝试将其分解成Applicative以了解其中的原因。

对于此主题还有一些有趣且有用的细节,然而,我只是想给你提供这个经验法则,通过它,您可以快速浏览一个do 推导式,并确定是否可以将其分解为Applicative风格的代码。

[1] 出现在<-左侧的部分。

[2] 出现在<-右侧的表达式。

[3] 严格地说,其中的一部分可以通过分解出e2 a来实现。


15
如果你喜欢 applicative 风格,有时可以混合使用 Monad 和 Applicative 运算符。第二个 do 块可以写成 f <$> (e1 >>= e2) <*> e3 - AndrewC
3
当需要具体示例来说明何时使用Applicative足够,何时需要Monad时,请点赞(+1)。 - sjy

45

基本上,单子也是适用函子 [1]。因此,每当您发现自己使用 liftMliftM2 等时,您可以使用 <*> 将计算链接在一起。在某种意义上,您可以将适用函子视为类似于函数的东西。纯函数 f 可以通过执行 f <$> x <*> y <*> z 来提升。

与单子相比,适用函子无法有选择地运行其参数。所有参数的副作用都会发生。

import Control.Applicative

ifte condition trueClause falseClause = do
  c <- condition
  if c then trueClause else falseClause

x = ifte (return True) (putStrLn "True") (putStrLn "False")

ifte' condition trueClause falseClause = 
  if condition then trueClause else falseClause

y = ifte' <$> (pure True) <*> (putStrLn "True") <*> (putStrLn "False")

x 只输出 True,而 y 则按顺序输出 TrueFalse

[1] The Typeclassopedia。强烈推荐。

[2] http://www.soi.city.ac.uk/~ross/papers/Applicative.html。虽然这是一篇学术论文,但很容易理解。

[3] http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors。讲解得非常清楚。

[4] http://book.realworldhaskell.org/read/using-parsec.html#id652399。展示了单子 Parsec 库如何以应用方式使用。


4
那是我见过唯一一个解释MonadApplicative之间区别的地方(就它们各自可以表达的内容而言)。干得好!我们可以用可应用函数来调用ifte吗:z = ifte <$> ... - Daniel
1
ifte已经是一个单子函数,所以再次提升它没有太大的意义。 - Wei Hu
3
你可以将"ifte' <$> (pure True)"写成"ifte' True"——在使用"<$>"和"<*>"时,"pure"很少必要(如果需要的话)。 - Martijn
1
y = ifte' True <*> putStrLn "True" <*> putStrLn "False"y = ifte' True <*> putStrLn "True" <*> putStrLn "False" - Edward Kmett
当你去掉 pure 时,它就变成了 y = ifte' True <$> (putStrLn "True") <*> (putStrLn "False") - David

10

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接