"效果明显"的确切含义是什么?

47
一次又一次地,我读到了“effectful”这个词,但我仍然无法清晰地定义它的含义。我猜正确的语境是“effectful计算”,但我也看到过“effectful(values)”这个术语。
我曾经认为“effectful”意味着“具有副作用”。但在Haskell中,除了某种程度上的IO之外,没有副作用。但是到处都是“effectful计算”。
然后我读到,单子被用于创建“effectful计算”。我可以在“State”单子的上下文中有些理解。但是我没能看出在“Maybe”单子中有任何副作用。总的来说,对我而言,把函数式的东西包装在单子中的单子比只包装一个值的单子更容易理解为产生副作用。
当谈到应用函子(Applicative functors)时,我更加困惑。我一直将应用函子视为以多个参数对函数进行“映射”的方式。我看不到任何副作用。或者说,“具有效果”和“带有效果”之间有什么区别吗?

或许有用:https://slpopejoy.github.io/posts/Effectful01.html 他将effectful定义为:1. 实际的副作用(IO)2. 看起来像副作用的东西(State,Writer等)3. 在函数调用之间持续存在的上下文(Reader,State等)4. 非局部控制流(Maybe,Either)。 - T.J. Crowder
3
大多数Monad都是基于纯计算定义的,但从程序员的角度来看,将State s a视为类似于命令式子程序的函数很常见。它可以读/写状态s并带有副作用,最后生成一个a。即使它是纯的,有时候假装它不是也是很方便的。关于主题,我认为“effectful”和“with effects”的意思是相同的,即使它们有时被广泛地应用于任何Monad / Applicative包装的值,即使这些值并没有建模副作用。 - chi
我在关于Traversables的问题的评论中找到了一个相当不错的答案。问题是“术语‘effect’是什么意思?” @missingfaktor 给出了一个非常好的回答,简而言之:“它指的是Functor的结构信息,即不是参数化的部分”。这里是完整的答案。 - Martin Drautzburg
“效果”在“包装”中。使用liftA2将函数“隐藏”,包装的效果被合并成一个组合的“有副作用”的包装值。而且,pure id <*> x = xpure创建了一个“最小”的包装/效果,其存在于链中不会改变任何东西。 - Will Ness
5个回答

30

副作用是与其环境的可观察互动(除了计算结果值)。在 Haskell 中,我们努力避免具有此类副作用的函数。即使是IO操作也适用:当评估IO 操作时,不会执行任何副作用,只有在main中执行IO值中所指定的操作时才执行。

然而,在处理与组合计算相关的抽象时(例如应用函子和单子),有必要在某种程度上区分实际值和“剩余部分”,我们通常称之为“效果”。特别地,如果我们有一种kind* -> * 的类型f,则在f a中,a部分是“值”,而其他“剩余部分”则是“效果”。

我有意引用这些术语,因为(据我所知)没有明确的定义,它只是一个口头定义。在某些情况下,没有任何值,或者有多个值。例如,在Maybe中,“效果”是可能没有值(计算被中止),在[]中,“效果”是有多个(或零个)值。对于更复杂的类型,这种区别可能会更加困难。
“效果”和“值”的区别实际上并不取决于抽象程度。 FunctorApplicativeMonad仅提供了我们可以使用它们的工具(Functor允许修改内部的值,Applicative允许组合效果,而Monad允许效果依赖于先前的值)。但在Monad的上下文中,更容易创建正在发生的事情的心理图像,因为单调动作可以“看到”先前计算的结果值,正如所见。
(>>=) :: m a -> (a -> m b) -> m b

操作符:第二个函数接收类型为a的值,因此我们可以想象“先前的计算产生了某些影响,现在有了其结果值,我们可以对其进行某些操作”。


2
你能否更详细地解释一下「作用器」的「效应」是什么?毕竟有一篇论文叫做「带着效应的作用器编程」,但我从来没有完全理解标题的含义(尽管我相信我理解了作用器)。 - Martin Drautzburg
4
如我所写,区分“effects”和“values”的方法是由实际数据类型而非我们用来处理它的抽象决定的。例如,对于Maybe来说,“effect”总是一个计算可能会被中止的效果,无论我们使用Applicative还是Monad(或其他)接口。如果你在Maybe中写入f <$> a <*> b <*> c,那么abc中的任何一个都可以通过返回Nothing来中止计算,这就是它的“effect”。Applicative接口允许组合效果(对于Maybe来说,这意味着在第一个Nothing处中止),但不允许一个效果依赖于先前的值。 - Petr

18
支持Petr Pudlák的回答,以下是关于那里所支持的更广泛的“效果”概念起源的论点。
短语“effectful programming”出现在McBride和Patterson的Applicative Programming with Effects摘要中,这篇论文介绍了applicative functors:
“在本文中,我们介绍了Applicativefunctors——一种弱于Monad但更为普遍的effectful programming应用风格的抽象特征。”
“Effect”和“effectful”在该论文的其他几个段落中也出现过;这些发生被认为不足以需要明确的澄清。例如,在Applicative的定义呈现之后(第3页)就做出了这样的评论:
在每个示例中,都有一个类型构造函数f,它嵌入了通常的值概念,但支持其自己独特的方式来赋予通常应用语言意义[...]我们相应地引入了Applicative类:
[一个Applicative的Haskell定义]
该类将SK(即S和K组合子,它们出现在Reader/函数Applicative实例中)从线程化环境推广到线程化一般效果。
从这些引用中,我们可以推断,在这个上下文中:
- 效果是Applicative“一般情况下”线程化的东西。 - 效果与获得Applicative实例的类型构造函数相关联。 - Monad也处理效果。
跟随这些线索,我们可以追溯回到至少Wadler的关于单子的论文。例如,以下是《函数式编程的单子》第6页的引用:
一般情况下,类型为a → b的函数将被类型为a → M b的函数替换。这可以理解为一个函数接受类型为a的参数并返回类型为b的结果,其中可能包含由M捕获的附加效果。该效果可以是状态操作、输出生成、异常引发等。从同一篇论文的第21页可以得知,如果单子封装了效果并且列表形成了一个单子,那么列表对应于某种效果。实际上,它们确实如此,并且它们对应的效果是选择。可以将类型为[a]的计算视为提供值的选择,每个元素都有一个值。类型为a → b的单子等效于类型为a → [b]的函数。这里的“对应于某种效果”是关键词语。它与摘要中更直接的声明相呼应:“单子提供了一个方便的框架,用于模拟其他语言中发现的效果,例如全局状态、异常处理、输出或非确定性。”
这里的意思是,单子可以用来表达在“其他语言”中通常编码为副作用的事物——即,正如Petr Pudlák在这里所说,“与[函数]环境的可观察交互(除计算其结果值外)”。通过转喻,这很容易导致“效果”获得第二个更广泛的意义,比“副作用”更广泛,即通过类型构造引入的任何东西,该类型构造是一个Monad实例。随着时间的推移,这个意义进一步概括为涵盖其他函数类别,如McBride和Patterson的工作中所看到的Applicative

总之,在Haskell术语中,“effect”有两个合理的含义:

  • 一个“字面”或“绝对”的含义:效果是副作用;以及

  • 一个“广义”或“相对”的含义:效果是函子上下文。

有时候,当每个相关方暗示不同的“效果”含义时,可能会发生可避免的术语分歧。另一个可能引起争议的地方是,在处理Functor而不是子类(如ApplicativeMonad)时,是否可以合法地谈论效果(我认为可以这样做,并与Petr Pudlák对为什么适用的函子可以具有副作用,但函子不能?达成一致)。


6
在我看来,“副作用”指的是正常函数无法完成的任何操作。换句话说,除了返回值之外的任何其他操作。
考虑以下代码块:
let
  y = foo x
  z = bar y
in foobar z

这个调用了foo,然后调用bar,最后调用foobar,这三个是普通的函数。看起来很简单,对吧?现在考虑下面这个例子:

do
  y <- foo x
  z <- bar y
  foobar z

这段代码调用了三个函数,但对于每一对连续的行之间也隐含地调用了(>>=)。这意味着,取决于函数运行在什么类型的单子中,会发生一些奇怪的事情。
  • 如果是identity单子,不会发生特殊的事情,单子版本与纯版本执行完全相同,没有副作用。

  • 如果每一个函数都返回Maybe-something,则如果(假设)bar返回Nothing,则整个屏幕块将中止。普通函数无法做到这一点。(即,在纯版本中,无法防止调用foobar。)因此,该版本执行了纯版本不能执行的操作。每个函数可以返回值或者中止屏幕块。这是一个副作用。

  • 如果每个函数返回something的列表,则代码会对所有可能的结果组合进行执行。同样,在纯版本中,无法让任何函数使用不同的参数多次执行。所以这是一个副作用。

  • 如果每个函数运行在状态单子中,那么(例如)foo可以直接将一些数据发送给foobar,除了你可以看到通过bar传递的值之外。再次,你无法使用纯函数来实现这一点,因此这是一个副作用。

  • 在IO单子中,你有各种有趣的效果。你可以将文件保存到磁盘(文件基本上是一个巨大的全局变量),你甚至可以影响其他计算机上运行的代码(我们称之为网络I/O)。

  • ST单子是IO单子的缩小版,它允许可变状态,但自包含的计算不能相互影响。

  • STM单子允许多个线程彼此交流,并可能导致代码多次执行,而且......你无法使用正常函数实现任何这些功能。

  • continuation单子可以让你“打破人们的思维”!纯函数理论上也可以达到这一点......


1
我同意你所描述的连续单子的“效果”。除此之外,我不太赞同你的观点。事实上,“do”符号隐藏了对>>=的调用,并不会使其具有效果,因为没有这种语法糖,表达式仍然是具有效果的,只是没有隐藏>>=。它根据单子类型的不同而执行不同的操作,这归功于单子是一个类型类。所有类型类都会发生这样的事情,无论是否具有效果。 - Martin Drautzburg
好的,显然 IOSTSTM 只能通过执行普通 Haskell 代码无法完成的低级操作来实现。严格来说,所有其他单子都只是纯代码被隐式运行。但仍然有用将它们视为具有效果的计算。在某种程度上,这只是一种观点问题。 - MathematicalOrchid
同意Orchid的观点。对于单子,有影响的语义是面向用户的预期语义,而实际上Haskell程序员往往从影响的角度思考。通过纯函数实现通常只是一个实现细节。尽管源码表示具有纯度,但在机器代码中我们可以进一步使用大量可变更新。因此,在纯函数层面停留是相当随意的。 - András Kovács
1
一个广泛的主题是,如果存在一个 x :: a 使得 m = pure x,那么计算 m :: f a 是纯的。在这个意义上不纯的任何东西都有“副作用”。当 f ~ Identity 时,每个计算都是纯的,因为创建类型 Identity a 的唯一方法是将 Identity = pure 应用于类型 a 的某些内容。 - dfeuer
@dfeuer,你的答案看起来很有趣,但我没有完全理解它。@Orchid&Andreas:我理解你(好)的观点,“effectful”是一种观察事物的方式,而不是你可以在代码中找到的东西。但我仍然不明白哪种观察事物的方式能够证明术语“effectful”。 - Martin Drautzburg

0

"Effect" 是一个非常模糊的术语,这是可以理解的,因为我们试图谈论的是超出语言范畴的东西。"Effect" 和 "side effect" 不是同一回事。"Effects" 是好的,而 "side effects" 则是错误的。

它们在词汇上的相似性真的很不幸,因为当人们阅读有关它们的文章时,会导致很多人混淆这些概念,并且人们会使用其中之一而不是另一个,从而导致很多混淆。

更多信息请参见:https://www.slideshare.net/pjschwarz/rob-norrisfunctionalprogrammingwitheffects


0

函数式编程的背景

Effect 通常指实现在 Applicative/Monad 实例中的东西(行为、额外逻辑)。

也可以说,简单值被扩展了额外的行为。

例如,

Option 模拟了可选性的效果

或者

Option 是模拟可选性效果的单子(monad)

资源: 资源1, 资源2


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