如何使用Applicative处理副作用?

mb1*_*b14 6 monads haskell 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")]
Run Code Online (Sandbox Code Playgroud)

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

这是否意味着>>可以写出Applicative类似的东西

do
  print' "hello"
  print' "world"
Run Code Online (Sandbox Code Playgroud)

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

在另一个世界,之间的区别Monad,并ApplicativeMonad允许x <- ...,但Applicative没有.

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

And*_*ewC 14

产量

适用的等价物>>*>,所以你可以做到

ghci> :m Control.Applicative
ghci> print 5 *> print 7
5
7
Run Code Online (Sandbox Code Playgroud)

输入 - 适用的更好案例

import Control.Applicative

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

getCompany :: IO Company
getCompany = Company <$> getLine <*> readLn
Run Code Online (Sandbox Code Playgroud)

哪个适用于输入:

ghci> getCompany >>= print
BigginsLtd
3
Company {name = "BigginsLtd", size = 3}
Run Code Online (Sandbox Code Playgroud)

请注意,由于我们使用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)
Run Code Online (Sandbox Code Playgroud)

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

(没有System.IO.reformat或者WinXP.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:\"
Run Code Online (Sandbox Code Playgroud)

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

不要重新格式化硬盘? 是的

.........your hard drive C: has been successfully reformatted.
"OK I didn't"
Run Code Online (Sandbox Code Playgroud)

  • -1用于停止使用Windows XP重新格式化硬盘驱动器.+2至少考虑它... (3认同)
  • 您的 `reformat` 示例完美地说明了 Applicative 和 Monad 之间的区别!到现在都没看到。:) :) :) (2认同)

Ben*_*Ben 5

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

用于组合的Applicative接口只允许您组合有效的值,以便产生的有效值根据一些"固定"配方组合它们的所有效果.

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

例如,State Integermonad/applicative具有依赖于(并影响)某些Integer状态的值.State Integer t值只有在该状态存在时才具有具体值.

这需要两个函数State Integer Char的值(叫他们ab),使我们回State Integer Char值,仅使用的应用型接口State Integer必须出示其"有状态"始终是相同的值,无论什么Integer状态值,也不管什么Char重视投入产量.例如,它可以将状态穿过a,然后以某种方式b组合它们的Char值.或者它可以通过b然后威胁国家a.或者它可以只选择a或仅选择b.或者它可以完全忽略它们,不会对当前Integer状态采取任何影响,只考虑pure一些char值.或者它可以以任何固定的顺序运行其中任何一个或两个任意固定的次数,并且它可以包含State Integer t它所知道的任何其他值.但无论它做什么,它总是这样做,无论当前Integer状态如何,或者State Integer t它设法得到的任何值产生的任何值.

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

所以是的,计算如下:

do
  print' "hello"
  print' "world"
Run Code Online (Sandbox Code Playgroud)

是否可以仅使用Applicative接口实现任何print'返回.你接近纠正Monad和Applicative之间的区别,如果两者都有一个do-notation将是monadic do允许的x <- ...,而applicative不会.虽然它比这更微妙; 也适用于Applicative:

do  x <- ...
    y <- ...
    pure $ f x y
Run Code Online (Sandbox Code Playgroud)

申请人不能做的是检查 xy决定f要对他们进行什么调用(或者做除了它f x y之外的其他结果pure.

但是,Writer w作为一个monad和一个应用程序之间没有区别,你并不完全正确.确实,monadic接口Writer w不允许所产生的依赖于效果("log"),因此必须始终可以将任何Writer w使用monadic功能定义的功能重写为仅使用应用功能并始终产生相同的价值2.但是monadic界面允许效果依赖于应用程序界面所不具备的,因此您不能总是忠实地再现Writer w仅使用应用程序界面的效果.

看到这个(有点傻)的示例程序:

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

然后在GHCi中加载:

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

请注意divM,是否numer包含的效果numer `divM` denom取决于denom(的效果tell ["divide by zero"]).尽管应用界面可以做到最好,但是效果numer总是包含在numerdivA中denom,即使懒惰的评估意味着从未检查过所产生的numer.当分母为零时,不可能在日志中添加"除以0".


1我不喜欢将"有效的价值观"结合起来作为那些monad和applicatives 的定义,但它是你可以用它们做什么的一个例子.

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