【问题标题】:Why can applicative functors have side effects, but functors can't?为什么应用函子可以有副作用,但函子不能?
【发布时间】:2013-01-12 15:32:57
【问题描述】:

我觉得问这个问题有点傻,但我一直在想这个问题,但我找不到任何答案。

所以问题是:为什么应用函子可以有副作用,而函子没有?

也许他们可以,但我从未注意到...?

【问题讨论】:

  • “有副作用”是什么意思?
  • 每个 monad 也是一个函子,所以你必须更具体...
  • 原始论文“Applicative Programming with effects”具有高度可读性,并通过几个示例解释了应用函子提供的额外强大功能。 strictlypositive.org/Idiom.pdf
  • 他们可以而且您从未注意到!我喜欢使用带有 IO 的 Functor 来应用这样的纯函数:fmap (unlines.reverse.lines) $ readFile "chronological_log.txt"。我倾向于对从Data.Functor(或Control.Applicative!)导入的fmap<$> 使用中缀运算符,所以我通常会写unlines.reverse.lines <$> readFile "chronological_log.txt"

标签: haskell monads side-effects applicative


【解决方案1】:

让我们首先将 side effects 重命名为 effects。各种价值观都会产生影响。仿函数是一种允许您将函数映射到由该效果产生的任何内容的类型。

当函子不适用时,它不允许您为效果使用某种组合风格。让我们选择一个(人为的)示例:

data Contrived :: * -> * where
    AnInt :: Int -> Contrived Int
    ABool :: Bool -> Contrived Bool
    None  :: Contrived a

这很容易成为一个仿函数:

instance Functor Contrived where
    fmap f (AnInt x) = AnInt (f x)
    fmap f (ABool x) = ABool (f x)
    fmap _ None      = None

然而,pure 没有合理的实现,所以这种类型不是应用函子。它与Maybe 相似之处在于它具有可能没有结果值的效果。但是你不能使用应用组合器来组合它。

【讨论】:

  • 这不是一个有效的Functor 实例。 (f x) 在前两种情况下不是正确的类型。使用fmap 唯一可以做的就是返回None。您可以类似地编写pure = const None 来创建Applicative 实例。
  • @John:你是对的。这甚至不是一个有效的函子,所以pure 的实现选择是没有实际意义的。你不能满足fmap id = id 法则。此外,您选择的pure 也不满足pure id <*> x = x 法则。
【解决方案2】:

这个答案有点过于简单了,但是如果我们将副作用定义为受先前计算影响的计算,很容易看出 Functor 类型类不足以解决副作用,因为没有办法链接多个计算。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

仿函数唯一能做的就是通过一些纯函数a -> b改变计算的最终结果。

但是,应用函子添加了两个新函数,pure<*>

class Functor f => Applicative f where
    pure   :: a -> f a
    (<*>)  :: f (a -> b) -> f a -> f b

&lt;*&gt; 是这里的关键区别,因为它允许我们链接两个计算: f (a -&gt; b)(产生函数的计算)和f a 的计算 提供应用函数的参数。使用 pure&lt;*&gt; 它是 可以定义例如

(*>) :: f a -> f b -> f b

这只是链接两个计算,丢弃第一个计算的最终结果 (但可能会应用“副作用”)。

简而言之,链接计算的能力是计算中可变状态等效果的最低要求。

【讨论】:

  • 谢谢shang,我怀疑它与连锁效应有关,但我不确定。感谢您把它写出来!
  • 如果你“将副作用定义为受先前计算影响的计算”,那么你可以争辩说 Applicative 也没有副作用——你需要一个完整的 monad。 Applicative 将它们链接在一起,但 Monad 允许第一个的输出影响第二个执行的操作。我现在想不出一个办法来改写我认为你的意思。 (我认为您的意思是像 flip const &lt;$&gt; writeFile "temp.txt" "Hello" &lt;*&gt; readFile "temp.txt" 这样的东西,其中第一个的动作会影响第二个的结果。)
  • 是的,更准确地说,对于应用程序,计算的最终结果可能会受到先前计算的影响。与之前计算的最终结果可用于选择下一个计算的 monad 不同。
【解决方案3】:

Functors 没有效果是不正确的。每个Applicative(以及每个MonadWrappedMonad)都是Functor。主要区别在于ApplicativeMonad 为您提供了如何使用这些效果以及如何组合它们的工具。大概

  • Applicative 允许您对效果进行排序并在其中组合值。
  • Monad 还可以让你根据上一个效果来决定下一个效果。

但是Functor 只允许你修改里面的值,它没有提供工具来做任何效果。因此,如果某事只是Functor 而不是Applicative,这并不意味着它没有效果。它只是没有一种机制如何以这种方式组合它们。

更新:举个例子,考虑

import Control.Applicative

newtype MyF r a = MyF (IO (r, a))

instance Functor (MyF r) where
    fmap f (MyF x) = MyF $ fmap (fmap f) x

这显然是一个带有效果的Functor 实例。只是我们没有办法定义符合Applicative 的具有这些效果的操作。除非我们对r 施加一些额外的约束,否则无法定义Applicative 实例。

【讨论】:

  • 出色的中肯答案,清楚地总结了 Functor、Applicative 和 Monad 类。如果可以的话,我会多次投票。
  • 谢谢彼得!这是一个很好的解释。我接受了另一个答案,因为它有更多的选票,但我认为这很棒。我很欣赏这个例子。
【解决方案4】:

这里的其他答案正确地表明,仿函数不允许副作用,因为它们不能组合或排序,这在很大程度上是正确的,但是有一种方法可以对仿函数进行排序:向内。

让我们写一个有限的 Writer 函子。

data Color    = R    | G    | B
data ColorW a = Re a | Gr a | Bl a deriving (Functor)

然后对它应用 Free monad 类型

data Free f a = Pure a | Free (f (Free f a))

liftF :: Functor f => f a -> Free f a
liftF = Free . fmap Pure

type ColorWriter = Free ColorW

red, blue, green :: a -> ColorWriter a
red   = liftF . Re
green = liftF . Gr
blue  = liftF . Bl

当然,通过 free 属性,这形成了一个 monad,但效果确实来自函子的“层”。

interpretColors :: ColorWriter a -> ([Color], a)
interpretColors (Pure a) = ([], a)
interpretColors (Free (Re next)) = let (colors, a) = interpretColors next
                                   in (R : colors, a)
...

所以,这是一种诡计。实际上,“计算”是由免费的 monad 引入的,但计算的材料,隐藏的上下文,只是由一个函子引入的。事实证明,您可以对任何数据类型执行此操作,它甚至不必是 Functor,但 Functor 提供了一种清晰的构建方式。

【讨论】:

    猜你喜欢
    • 2018-12-02
    • 1970-01-01
    • 2013-01-24
    • 2018-07-15
    • 2017-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多