【问题标题】:How to handle side effect with Applicative?如何处理 Applicative 的副作用?
【发布时间】:2014-08-19 12:10:03
【问题描述】:

我到处都看到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 一样),但您不能重用先前副作用的结果。

这是否意味着&gt;&gt; 可以写成Applicative 之类的东西

do
  print' "hello"
  print' "world"

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

在其他世界中,MonadApplicative 之间的区别在于 Monad 允许 x &lt;- ...Applicative 不允许。

那么,Writer monad 是否只是一个应用程序?

【问题讨论】:

  • @CarstenKönig:+1 链接。但是我现在更加困惑了。
  • (&gt;&gt;) 确实存在于应用程序中,称为(*&gt;),因此您可以使用print "hello" *&gt; print "world"
  • @CarstenKönig 我通常建议人们在大量使用 do 这样的表示法时切换到 Applicative,因为 Applicative 表示法感觉更纯粹且不那么混乱。该提议与我的建议相反!
  • @AndrewC,这看起来很奇怪。我认为选择一个新的关键词更明智,比如doA

标签: haskell monads applicative


【解决方案1】:

输出

&gt;&gt; 的应用等价物是*&gt;,所以你可以这样做

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

输入 - Applicative 的更好案例

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}

请注意,由于我们对 IO 使用 Applicative,所以无论如何我们都在 IO monad 中,所以如果我们愿意,可以使用 &gt;&gt;=。 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 有&gt;&gt;=,它可以让你根据自己的值选择使用什么副作用。

使用 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。这只是我觉得有趣的一个例子。)

使用应用程序:

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 中确定,并且硬盘驱动器将 总是用这段代码重新格式化。我需要 Monad 的绑定 (&gt;&gt;=) 才能停止重新格式化。

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

【讨论】:

  • -1 用于停止重新格式化装有 Windows XP 的硬盘驱动器。 +2 至少考虑一下...
  • @leftaroundabout 呵呵。我很高兴你幽默的点赞让我成为 20k 用户!谢谢。 :)
  • 值得一读的问题What advantage does Monad give us over an Applicative?,它对一个密切相关的问题有一些很好的答案。
  • 您的 reformat 示例完美地说明了 Applicative 和 Monad 之间的区别!直到现在都没有看到。 :) :) :)
【解决方案2】:

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

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

但是,它不能使列表之一依赖于前一个列表中包含的值。为此,您需要 Monad 实例。

例如,考虑代码:

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

这里我们生成列表,其值取决于计算中较早出现的列表([2,7] 之一)。 Applicative 无法做到这一点。

类比

我希望下面的比喻不是太迟钝,但Applicatives 就像铁路。

Applicative 中,效果构建了功能应用程序的机车将在其上行驶的铁路。所有效果都发生在修建铁路时,而不是在机车移动时。机车不能以任何方式改变其路线。

Monad 就像在仍在建设中的铁路中安装机车。乘客实际上可以向前方几米处的施工人员大喊,告诉他们“我喜欢这边的风景,请将铁轨往右一点”或“你可以停止铺设铁轨,我就在这里出去” ”。当然,对于这条陌生的铁路,公司无法提供上车前可以查到的时刻表或固定的停靠站清单。

这就是为什么Applicatives可以被视为一种“广义函数应用”的形式,而Monads却不能。

【讨论】:

  • 单子的完美比喻!那个,或者一个管道网络,就像东西/数据在其中流动一样,基于那个东西/数据流。 --- --- --- 也许 Monads 可以 被认为是更通用的 函数应用! :)
  • 实际上,您的foo 可编码为应用程序-因为两个替代列表的长度是相同的!只有当下一个计算的“形状”可以根据前一个计算中的值而​​改变时,它才真正是一个整体的单子计算。
  • @WillNess 效果(此处为非确定性)取决于先前有效操作返回的 。对应的Applicative 表达式是什么?
  • foo = liftA2 g [2,7], [0,1,2] where g r i = if (even r) then [2,5,6] !! i else [9,234,343] !! i。 :) shape 是预先知道的,所以,它可以完成!如果你从r 计算了这些列表中的值,你仍然可以在g 中计算它们。只有当 shape 由值确定时,它才变得不可能。所以通常谈论join 太简单了。 join 的一些用法仍然适用。只有当join 不可避免时,计算为essentially monadic。 :)
  • @WillNess 这意味着不同的 monad 不仅在它们的上下文中有所不同,而且在它们对什么构成该上下文中的相关值的概念上也有所不同。在非确定性列表上下文中,不是列表值本身是相关的,而是它们分别存在和不存在。令人兴奋!
【解决方案3】:

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

用于组合的 Applicative 接口只允许您组合有效值,以便生成的有效值根据一些“固定”配方组合所有效果。

用于组合的 Monad 接口允许您以这样一种方式组合有效值,即组合值的效果取决于原始有效值在实际解析时的作用。

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

一个函数接受两个State Integer Char 值(称它们为ab)并返回一个State Integer Char 值并且只使用State Integer 的Applicative 接口必须产生一个“有状态”的值无论Integer 状态值是什么,也无论输入产生什么Char 值,总是相同的。例如,它可以通过ab 将状态线程化,以某种方式组合它们的Char 值。或者它可能通过b 然后a 威胁国家。或者它可以只选择a 或只选择b。或者它可以完全忽略两者,而不会对当前的Integer 状态产生任何影响,而只是对pure 一些字符值产生影响。或者它可以以任何固定顺序运行它们中的任何一个或两个,并且它可以包含它知道的任何其他State Integer t 值。但无论它做什么,它总是这样做,不管当前的Integer 状态,或者它设法获得的任何State Integer t 值产生的任何值。

一个接受相同输入但能够为State Integer 使用monad 接口的函数可以做的远不止这些。它可以运行ab,具体取决于当前Integer 状态是正还是负。它可以运行a,然后如果生成的Char 是一个ascii 数字字符,它可以将数字转换为数字并多次运行b。以此类推。

所以是的,计算如下:

do
  print' "hello"
  print' "world"

对于print' 返回的任何内容,都可以仅使用 Applicative 接口来实现。如果 Monad 和 Applicative 都有一个 do-notation,那么您接近于正确的区别在于 monadic do 将允许 x &lt;- ...,而 applicative do 则不允许。它比这更微妙一些。 this 也适用于 Applicative:

do  x <- ...
    y <- ...
    pure $ f x y

Applicative 不能做的是 检查 xy 来决定 f 调用它们(或者对 f x y 的结果做任何事情,而不仅仅是 @ 987654358@它。

但是,Writer w 作为 monad 和作为应用程序之间没有区别,您并不完全正确。 Writer w 的单子接口确实不允许 value 依赖于效果(“日志”),因此必须始终可以重写任何使用定义的 Writer w monadic features 到只使用 applicative features 并始终产生相同值的一种方法2。但是 monadic 接口允许 效果 依赖于 values,而 applicative 接口则不允许,因此您不能总是忠实地重现 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

然后在 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"])

注意divMnumer的效果是否包含在numer `divM` denom中取决于denom的值(tell ["divide by zero"]的效果是否包含)。在应用程序接口所能做的最好的情况下,numer 的效果总是包含在numerdivAdenom 中,即使惰性求值意味着numer 产生的 永远不会检查。当分母为零时,不可能在日志中添加“除以 0”。


1 我不喜欢像 monad 和 applicatives 的定义那样认为“组合有效值”,但这是一个 示例 em> 你可以用它们做什么。

2 无论如何,当不涉及底部时;你应该可以从我的例子中看出为什么底部会搞乱等价。

【讨论】:

    猜你喜欢
    • 2014-02-08
    • 2016-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-25
    • 1970-01-01
    • 2020-12-04
    相关资源
    最近更新 更多