什么是“副作用”?


Answers:


108

一个副作用仅仅指的是某种状态的改变-例如:

  • 更改变量的值;
  • 将一些数据写入磁盘;
  • 在用户界面中启用或禁用按钮。

某些人似乎说的相反

  • 副作用并没有要隐藏的或意外的(也可以是,但有没有关系,因为它适用于计算机科学的定义);

  • 副作用与幂等性无关。幂等函数可能有副作用,而非幂等函数可能没有副作用(例如获取当前系统日期和时间)。

这真的非常简单。副作用=在某处更改某些内容。

PS正如评论者benjol指出的那样,可能会有几个人将副作用的定义与纯函数的定义混淆,纯函数是(a)幂等且(b)没有副作用的函数。在一般计算机科学中,一种并不暗示另一种,但是功能编程语言通常倾向于强制执行这两种约束。


38
短语“副作用”使它听起来象是别人正在改变不是想要的结果等。在医学上,药物的主要作用是减轻疼痛,有时还具有引起流鼻血,头昏眼花等副作用。药物的目的不是引起流鼻血,而是有时以意外的额外结果。
FrustratedWithFormsDesigner

15
@沮丧:+1。每当我看到这个词时,我都会忍不住想问,FP倡导者是否没有选择它来精确地创造这种微妙的险恶含义。
梅森惠勒

6
@梅森·惠勒 它早在FP之前就已存在。这不是一个险恶的含义。这是邪恶的,而且一直存在。在我编写代码的30年中,“加密分配”声明(即副作用)一直困扰着人们。一个简单的旧赋值语句要容易得多。
S.Lott

7
@Mason惠勒:在C. ++a。看起来不像作业。 b = ++a;有两个副作用。的明显的一个和的密码分配a。这是(某些人)希望得到的副作用。但是一直被认为是我整个职业的副作用。
S.Lott

5
@Zachary,请参阅我的回答中的最后一个要点。您指的是幂等行为(或缺乏幂等行为)。那并没有告诉您任何有关副作用的信息。检查系统时钟不是副作用。实际上,任何以“ get”为前缀的函数或方法都是您应该合理预期不会产生任何副作用的函数或方法。
Aaronaught 2011年

36

修改计算机状态或与外界交互的任何操作都被认为具有副作用。参见有关副作用的维基百科。

例如,此功能没有副作用。其结果仅取决于其输入参数,而在调用该程序时,与程序状态或环境无关的内容不会改变:

int square(int x) { return x * x; }

相反,根据调用它们的顺序,调用这些函数将给您不同的结果,因为它们会改变计算机的状态:

int n = 0;
int next_n() { return n++; }
void set_n(int newN) { n = newN; }      

此功能具有将数据写入输出的副作用。您不调用该函数是因为您需要它的返回值。之所以称呼它是因为您希望它对“外部世界”具有影响:

int Write(const char* s) { return printf("Output: %s\n", s); }

1
这是一个很好的定义,但是我对它的详尽描述并不感到疯狂-正如索比约恩的回答一样,它的一部分似乎是将副作用与幂等函数混为一谈。如您的Write示例所示,具有副作用并不意味着该函数会相对于其输入更改其输出,甚至并不表示该函数的输出完全取决于输入。
亚伦诺特2011年

6
这不是关于幂等的。它产生输出的事实意味着它具有副作用。
克里斯托弗·约翰逊

在某些系统中,调用square(x)可能导致定义功能的模块从磁盘加载。应该认为这是副作用吗?毕竟,这男子的(第一)调用花费意外长,即RAM的使用上升等
哈根·冯·艾特森

1
@HagenvonEitzen实际上,每个操作都会更改计算机的状态(CPU寄存器,内存,功耗,热量等)。“副作用”通常是指虚构的理想执行环境,除非程序显式更改该环境,否则该环境不会改变。但是,如果square(x) 由于要更改外部计算机状态而进行呼叫,则可以认为这是副作用。
克里斯托弗·约翰逊

对我来说,第一个插图是很合理的。但是,第二个要少一些。我认为应该根据特定的环境/范围来定义副作用。如果您考虑整个宇宙,那么就没有副作用。即使将其限制在计算机上,您的功能也会影响其他进程,因为CPU的行为将不同。如果将作用域限制为局部函数作用域中可访问的事物,那么我们有话要说。
funct7

20

我认为现有的答案是相当不错的。我想在某些方面阐述海事组织的压力不够大。

在数学中,函数只是从值的元组到值的映射。因此,给定一个函数f和一个值xf(x)将始终是相同的结果y。你可能会取代f(x)y无处不在的表达,不会有任何变化。

在许多编程语言中,所谓的函数(或过程)是可以执行的构造(代码段),因为:

  1. 它从数学意义上计算一个函数,即给定输入值,它返回结果,或者
  2. 它会产生某种效果,例如在屏幕上打印内容,更改数据库中的值,发射导弹,睡眠10秒钟,发送SMS。

因此效果可能与状态有关,也可能与其他方面有关,例如发射导弹或暂停执行几秒钟。

副作用一词听起来可能是负面的,但通常调用函数的作用是函数本身的目的。我猜想,由于术语“函数”最初是在数学中使用的,因此计算值被认为是函数的主要作用,而其他任何作用都被认为是副作用。一些编程语言使用术语过程来避免与数学意义上的函数混淆。

注意

  1. 一些过程对于它们的返回值和副作用都是有用的。
  2. 某些过程仅计算结果值,而没有其他影响。它们之所以称为纯函数,是因为它们所做的只是在数学意义上计算一个函数。
  3. 某些过程(例如sleep(),Python中的过程)仅对其(副作用)有用。这些通常被建模为返回特殊值Noneunit()或...的函数,它们仅表示计算已正确终止。

2
以我的拙见,这应该是公认的答案。副作用的概念甚至仅在数学函数方面才有意义。该过程旨在简单地以结构化的方式对一组指令进行分组,同时使您可以从任何地方跳转到该指令集并方便地返回。没有主要的预期效果,也没有副作用。您可能会说抛出异常是过程的副作用,因为它破坏了过程的意图,即使您跳回到上次停止的地方并在那里继续执行。
Didier A.

4

副作用是操作对超出预期用途的变量/对象产生影响时。

当您调用具有更改某些全局变量的副作用的复杂函数时,可能会发生这种情况,即使这不是您调用它的原因(也许您调用它是为了从数据库中提取某些内容)。

我承认我很难提出一个看起来并不完全人为的简单示例,而且我所研究的内容中的示例太久了,无法在此处发布(并且由于与工作相关,因此我可能无论如何都不应该这样做) )。

我之前看到的一个示例是一个函数,如果连接处于关闭状态,则该函数将打开数据库连接。问题在于应该在函数末尾关闭连接,但是开发人员忘记添加该代码。因此,这里有一个意外的副作用:调用过程只应该执行查询,并且副作用是连接保持打开状态,如果连续两次调用该函数,则会引发错误,指出连接为已经打开。


好的,既然大家现在都在举一些例子,我想我也会;)

/*code is PL/SQL-styled pseudo-code because that's what's on my mind right now*/

g_some_global int := 0; --define a globally accessible variable somewhere.

function do_task_x(in_a in number) is
begin
    b := calculate_magic(in_a);
    if b mod 2 == 0 then
        g_some_global := g_some_global + b;
    end if;
    return (b * 2.3);
end;

该函数do_task_x具有一个返回一些计算的结果的影响,和一个侧面的可能修改的全局变量的效果。

当然,是主要的,也是副作用,可能需要解释,并且可能取决于实际用法。如果我出于修改全局变量的目的调用此函数,并且放弃返回的值,那么我会说修改全局变量是主要的作用。


2
我认为这不是一个很好的通用定义。许多程序员特意使用其副作用的构造。
CB Bailey

@查尔斯:足够公平。在这种情况下,您将如何定义它?
FrustratedWithFormsDesigner

2
我认为@KristopherJohnson具有最清晰的定义。改变程序状态或其环境或产生现实效果的任何事物,例如生成输出。
CB Bailey

@查尔斯·贝利:这不会改变定义。用东西做副作用很好。只要您了解其中就有副作用。它不会改变此定义的任何内容。
S.Lott

1
@SLott:此答案中的定义(即第一段)包括以下子句:“预期用途之外”。我认为我的评论是公正的。
CB Bailey

3

在计算机科学中,如果函数或表达式修改某些状态或与调用函数或外界有可观察的交互作用,则称其具有副作用。

来自维基百科-副作用

从数学意义上讲,函数是从输入到输出的映射。调用函数的预期效果是使函数将输入映射到返回的输出。如果该函数执行其他任何操作,则无关紧要,但是,如果该函数具有未将输入映射到输出的任何行为,则该行为被认为是副作用。

更一般地讲,副作用是不是构造设计者的预期效果的任何效果。

效果是影响演员的任何事物。如果我调用一个向女友发送分手短信的功能,则该功能会影响一堆演员,包括我,她,手机公司的网络等。调用该功能的唯一目的是该功能从我的输入返回一个映射。因此对于:

   public void SendBreakupTextMessage() {
        Messaging.send("I'm breaking up with you!")
   }

如果打算将其用作函数,则它唯一要做的就是返回void。如果没有副作用,则它实际上不应发送短信。

在大多数编程语言中,没有数学函数的构造。没有打算照此使用任何构造。这就是为什么大多数语言都说您有方法或过程的原因。通过设计,它们旨在能够产生更多的效果。用普通的编程术语来说,没有人真正关心方法或过程的意图,因此当有人说此函数有副作用时,他们实际上是在说,这种构造的行为不像数学函数。当有人说这个函数没有副作用时,他们的意思是说,这个结构实际上就像数学函数一样工作。

根据定义,纯函数始终无副作用。纯粹的函数可以说是一种函数,即使它使用的结构允许更多的效果,但实际上仅具有与数学函数相同的效果。

我挑战任何人告诉我什么时候无副作用的功能将不纯净。除非使用术语“纯”和“无副作用”的句子上下文的主要预期效果不是函数的数学预期效果,否则它们始终相等。

因此,有时(尽管很少),并且我认为这是公认的答案中缺乏区别并且误导人们的原因(因为这不是最常见的假设),但是有时假定编程功能的预期效果是将输入映射到输出,其中输入不限于函数的显式参数,而输出则限于显式返回值。如果您认为这是预期效果,那么由于允许输入来自预期效果中的其他位置,因此读取文件并基于文件中内容返回不同结果的函数仍然没有副作用。

那么,为什么这一切都很重要?

这一切都与控制和保持控制有关。如果调用一个函数,然后又执行其他操作然后返回一个值,则很难推断出其行为。您将需要在函数内部查找实际代码,以猜测其作用并断言其正确性。理想的情况是,很清楚并且很容易知道函数正在使用什么输入,并且它没有做任何其他事情,然后为它返回输出。您可以对此稍作放松,并说确切地知道它正在使用什么输入并不像确定它没有做任何您可能不知道的然后返回值的事情那样有用,所以也许您对仅执行感到满意它不执行任何其他操作,然后将输入(无论从何处获取)映射到输出。

在几乎所有情况下,程序的目的都是要产生影响,然后再将输入的内容映射到输出的内容上。控制副作用的想法是,您可以以一种易于理解和推理的方式来组织代码。如果将所有副作用汇总在一起,放在一个非常明确和重要的地方,就很容易知道在哪里看,并相信这就是正在发生的一切,仅此而已。如果您的输入也非常明确,则可以帮助测试不同输入的行为,并且使用起来更容易,因为您无需在很多不同的地方更改输入,只是其中一些可能并不明显。得到你想要的。

因为最有助于理解,推理和控制程序行为的地方是将所有输入清楚地分组在一起并且是明确的,并且将所有副作用都分组在一起并且是明确的,这通常是人们在谈论时所说的副作用,纯等

因为最有用的是对副作用及其明确性进行分组,所以有时人们只会表示此意思,并通过说它不是纯净的,而是仍然是“副作用”自由的来区分它。但是副作用是相对于假定的“预期的主要作用”的,因此它是一个上下文术语。我发现这种用法很少使用,尽管令人惊讶的是在该线程中谈论了很多。

最后,幂等意味着用相同的输入多次调用此函数(无论它们来自何处)始终会产生相同的效果(是否有副作用)。


我认为解释副作用的一个大问题是,除非您使用过像Ocaml或Haskell这样的语言,否则很难推理出无副作用的(几乎!)编程。
Jamie Strauss

2

在编程中,副作用是过程从其范围之外更改变量时。副作用与语言无关。有一些旨在消除副作用的语言(纯功能语言),但是我不确定是否有任何需要副作用的语言,但是我可能是错的。

据我所知,没有内部和外部的副作用。


更准确地说,纯功能语言将无副作用的代码与其他代码清楚地分开,而其他语言则没有区分纯代码和不纯代码的机制。大多数程序都必须具有副作用才能使用。
Giorgio

我认为,某些pre-gui编程语言,例如MS-BASIC和QBasic,可能已尽可能接近“仅副作用”语言。是的,您可能同时具有内部和外部副作用。
James K

0

这是一个简单的示例:

int _totalWrites;
void Write(string message)
{
    // Invoking this function has the side effect of 
    // incrementing the value of _totalWrites.
    _totalWrites++;
    Debug.Write(message);
}

副作用的定义并非特定于编程,因此只需想象一下药物或进食过多的副作用即可。


但是,如果将消息作为参考,而您在方法中更改消息,则可能会产生副作用。我对么?
Amir Rezaei

表达式x++修改变量的事实x通常被认为是副作用。表达式的值是的预增值x;这是表达式的非副作用部分。
CB Bailey

@Charles-我同意,尽管原始示例并不像当前示例那么清晰。
ChaosPandion 2011年

@Amir-好吧,这确实取决于语言。如果这是C#,则不会被视为副作用。
ChaosPandion 2011年

@ChaosPandion:我个人不同意。原始示例更加简单明了。
CB Bailey

-2

副作用是代码中发生的事情并不明显。

例如,假设您有这个课程

public class ContrivedRandomGenerator {
   public int Seed { get; set; }

   public int GetRandomValue()
   {
      Random(Seed);
      Seed++;
   }
}

最初创建类时,您要为其提供种子。

var randomGenerator = new ContrivedRandomGenerator();
randomGenerator.Seed = 15;
randomGenerator.GetRandomValue();

您不知道内部结构,只是希望获得一个随机值,并且您希望randomGenerator.Seed仍为15 ...但事实并非如此。

该函数调用具有更改种子值的副作用。


10
副作用不必隐藏。您正在考虑口语或医疗用途;在编程中,副作用只是指修改某些状态。
亚伦诺特2011年

1
打印到控制台是一个副作用。它不是隐藏的。摘自Wikipedia:“在计算机科学中,如果函数或表达式除了返回值之外还修改某些状态或与调用函数或外界具有可观察的交互作用,则该函数或表达式具有副作用。”

副作用是非函数(即过程)如何完成任何工作。X = 1; X = Y(10)是两个纯函数。当您走出“ x = what”领域时,是将输出写入屏幕还是在“ x = y”格式之外读取输入,还是将变量的值从一件事更改为另一件事? ,这是一个副作用。
James K

我认为“隐藏”是指不明显。像在x = f(y,z)中一样,可以假定x基于y和z。而proc(x,y,z)告诉您发生了什么。每个变量都可以更改,也可以不更改。Proc可以是f的类似物,也可以是完全不相关的。一个纯函数有一个答案:“ x”。除此之外,这是副作用。完全意图,但有副作用。
James K

就像了解0一样,您必须首先了解1:要了解副作用,必须首先了解功能。
James K
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.