如何使用Applicative处理副作用?

时间:2014-06-28 14:29:51

标签: haskell monads applicative

我看到Applicative可以处理副作用的所有地方,但我见过的所有简单例子都是将各种东西组合在一起,如:

> (,,) <$> [1,2] <*> ["a", "b", "c"] <*> ["foo", "bar"]
[(1,"a","foo"),(1,"a","bar"),(1,"b","foo"),(1,"b","bar"),
 (1,"c","foo"),(1,"c","bar"),(2,"a","foo"),(2,"a","bar"),
 (2,"b","foo"),(2,"b","bar"),(2,"c","foo"),(2,"c","bar")]

哪个很酷,但我看不出它与副作用的关系。我的理解是Applicative是一个弱monad,所以你可以处理副作用(就像你对State monad一样),但你不能重复使用前一个副作用的结果。

这是否意味着可以为>>

之类的内容编写Applicative
do
  print' "hello"
  print' "world"

会有意义(使用print' :: a -> Applicative something)(使用适当的do-applicative扩展名)。

在另一个世界中,MonadApplicative之间的区别是Monad允许x <- ...Applicative不允许。{/ p>

然后,是作家monad,只是一个应用程序?

3 个答案:

答案 0 :(得分:14)

输出

>>的应用等效项为*>,因此您可以

ghci> :m Control.Applicative
ghci> print 5 *> print 7
5
7

输入 - 适用于

的更好案例
import Control.Applicative

data Company = Company {name :: String, size :: Int}
  deriving Show

getCompany :: IO Company
getCompany = Company <$> getLine <*> readLn

适用于输入:

ghci> getCompany >>= print
BigginsLtd
3
Company {name = "BigginsLtd", size = 3}

请注意,由于我们正在使用Applicative for IO,我们无论如何都在IO monad中,所以如果我们愿意,可以使用>>=。 Applicative给你的好处是语法很好。

我最喜欢的是解析,所以我可以做

data Statement = Expr Expression | If Condition Statement Statement
parseStatement = Expr <$> parseExpression <|> 
                 If <$> (string "if" *> parseCondition) 
                    <*> (string "then" *> parseStatement)
                    <*> (string "else" *> parseStatement)

Applicative和Monad

之间的区别

Applicative和Monad之间的区别在于Monad有>>=,可让您根据自己的价值选择使用哪种副作用。

使用Monad:

don't_reformat_hard_drive :: Bool -> IO ()
don't_reformat_hard_drive yes = if yes then putStr "OK I didn't" 
                                       else putStr "oops!" >> System.IO.reformat "C:/"

maybeReformat :: IO ()
maybeReformat = WinXP.Dialogs.ask "Don't reformat hard drive?" 
               >>= don't_reformat_hard_drive 

(没有System.IO.reformatWinXP.Dialogs.ask。这只是我觉得有趣的一个例子。)

使用Applicative:

response :: Bool -> () -> String
response yes () = if yes then "OK I didn't" else "oops!"

probablyReformat = response <$> WinXP.Dialogs.ask "Don't reformat hard drive?"
                            <*> System.IO.reformat "C:\"

遗憾的是,使用Applicative我无法检查布尔值以确定是否重新格式化 - 副作用顺序是在编译时确定,在Applicative中,并且硬盘驱动器将< em>总是用这段代码重新格式化。我需要Monad的绑定(>>=)才能停止重新格式化。

Don't reformat hard drive? Yes No

.........your hard drive C: has been successfully reformatted.
"OK I didn't"

答案 1 :(得分:5)

Applicative和Monad都提供了将多个副作用 1 值“组合”成单个副作用值的方法。

用于组合的Applicative界面让您可以组合有效的值,以便产生的有效值根据某些“固定”配方组合所有效果。

用于组合的Monad接口允许您组合有效值,使组合值的效果取决于原始有效值在实际解析时的作用。

例如,State Integer monad / applicative的值取决于(并影响)某些Integer状态。 State Integer t值仅在存在该状态时具有具体值。

一个函数,它接受两个State Integer Char值(称为ab)并返回State Integer Char值并仅使用{{1}的Applicative接口无论State Integer状态值是什么,无论输入产生什么Integer值,都必须生成一个“有状态”始终相同的值。例如,它可以通过Char然后a来处理状态,并以某种方式组合它们的b值。或者它可以通过Char然后b来威胁状态。或者它只能选择a或仅a。或者它可以完全忽略它们,不对它们的当前b状态采取任何一种效果,而只采用Integer一些char值。或者它可以以任何固定的顺序运行其中任何一个或两个任意固定的次数,并且它可以包含它知道的任何其他pure值。但无论它做什么,始终都会这样做,无论当前的State Integer t状态如何,或者它设法获得的任何Integer值产生的任何值。

使用相同输入但能够使用State Integer t的monad接口的函数可以做更多的事情。它可以运行State Integera,具体取决于当前b状态是正还是负。它可以运行Integer,如果生成的a是一个ascii数字字符,它可以将数字转换为数字并多次运行Char。等等。

是的,计算如下:

b

是否可以使用Applicative接口实现任何do print' "hello" print' "world" 返回。你接近纠正了Monad和Applicative之间的区别,如果两者都有一个do-notation,那monadic会允许print',而申请则不会。虽然它比这更微妙; 也适用于Applicative:

x <- ...

申请人无法做的是检查 do x <- ... y <- ... pure $ f x y x来决定y要对他们进行调用(或对{的结果做任何事情{1}} {而不仅仅是f

你并不完全正确,f x y作为monad和appative之间没有区别。确实pure的monadic接口不允许产生依赖于效果(“log”),因此必须始终可以重写任何{{1}使用monadic功能定义仅使用应用功能并始终产生相同值 2 的功能。但monadic接口允许效果依赖于,而应用程序界面却没有,所以你不能总是忠实地再现{{1}的效果仅使用应用程序界面。

看到这个(有些愚蠢)的示例程序:

Writer w

然后加载GHCi:

Writer w

请注意Writer w的问题,Writer w的{​​{1}}效果是否包含在import Control.Applicative import Control.Monad.Writer divM :: Writer [String] Int -> Writer [String] Int -> Writer [String] Int divM numer denom = do d <- denom if d == 0 then do tell ["divide by zero"] return 0 else do n <- numer return $ n `div` d divA :: Writer [String] Int -> Writer [String] Int -> Writer [String] Int divA numer denom = divIfNotZero <$> numer <*> denom where divIfNotZero n d = if d == 0 then 0 else n `div` d noisy :: Show a => a -> Writer [String] a noisy x = tell [(show x)] >> return x 中取决于*Main> runWriter $ noisy 6 `divM` noisy 3 (2,["3","6"]) *Main> runWriter $ noisy 6 `divM` noisy 0 (0,["0","divide by zero"]) *Main> runWriter $ undefined `divM` noisy 0 (0,["0","divide by zero"]) *Main> runWriter $ noisy 6 `divA` noisy 3 (2,["6","3"]) *Main> runWriter $ noisy 6 `divA` noisy 0 (0,["6","0"]) *Main> runWriter $ undefined `divA` noisy 0 (0,*** Exception: Prelude.undefined *Main> runWriter $ (tell ["undefined"] *> pure undefined) `divA` noisy 0 (0,["undefined","0"]) 的值({1}的效果是否也是如此})。尽管应用界面可以做到最好,divM的效果总是包含在numer divA numer `divM` denom中,即使懒惰评估意味着产生了永远不会检查denom。当分母为零时,不可能在日志中添加“除以0”。


1 我不喜欢将“有效值组合”视为monad和applicatives的定义,但它是示例

2 无论如何都不涉及底部;你应该能够从我的例子中看到为什么底部会弄乱等价物。

答案 2 :(得分:4)

在您的示例中,正在使用列表的Applicative实例。这里的“效果”是非确定性:返回多个可能的值。

列表的Applicative实例计算各个列表参数中元素的可能组合。

然而,它不能做的是使其中一个列表依赖于前一个列表中包含的值。您需要Monad实例。

例如,考虑代码:

foo :: [Int]
foo = do
        r <- [2,7]
        if (even r)
             then [2,5,6]
             else [9,234,343]

这里我们生成的列表的值取决于计算中较早出现的列表([2,7])。你不能用Applicative

来做到这一点

类比

我希望以下类比不是太迟钝,但Applicative就像铁路一样。

Applicative中,效果构建了功能应用的机车将行进的铁路。所有这些效果都是在建造铁路时发生的,而不是在机车移动时。机车不能以任何方式改变它的路线。

A Monad就像在铁路上有一个仍在建设中的火车头。乘客实际上可以向前方几米处的建筑工作人员大喊,告诉他们“我喜欢这边的风景,请将轨道向右倾斜一点”或“你可以停止铺设轨道,我就在这里出去”。当然,对于这条奇怪的铁路,公司无法提供时间表或固定的停靠列表,可以在登上火车前进行检查。

这就是为什么Applicative可以被视为“广义函数应用”的一种形式,但Monad不能。{/ p>