纯函数:“无副作用”是否意味着“在给定相同输入的情况下始终相同的输出”?


84

定义函数的两个条件pure如下:

  1. 无副作用(即仅允许更改本地范围)
  2. 给定相同的输入,始终返回相同的输出

如果第一个条件始终为真,那么是否有第二次条件不为真?

即真的只有第一个条件才需要吗?


3
您的场所指定不正确。“输入”范围太广。可以认为函数有两种输入。他们的论点,以及“环境” /“背景”。如果您不区分这两种输入,则可以将返回系统时间的函数视为纯函数(即使它不被观察)。
亚历山大-恢复莫妮卡

4
@Alexander:在“纯函数”的上下文中,“输入”通常被理解为表示显式传递的参数/自变量(通过编程语言使用的任何机制)。这是“纯函数”定义的一部分。但是您是对的,记住定义很重要。
sleske

3
简单的反例:返回全局变量的值。没有副作用(只能读取全局!),但是每次都可能有不同的结果。(如果您不喜欢全局变量,请在运行时返回取决于调用堆栈的局部变量的地址)。
彼得-恢复莫妮卡

2
您需要扩展对“副作用”的定义;您说纯方法不会产生副作用,但是您还需要注意,纯方法不会消耗其他地方产生的副作用。
埃里克·利珀特

2
@sleske也许是人们普遍理解的,但是缺少这种区别是造成OP混乱的确切原因。
亚历山大-恢复莫妮卡

Answers:


114

以下是一些不会改变外部范围但仍被认为是不纯的反例:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (这确实会更改PRNG,但不能视为可观察到的)

访问非常量的非局部变量足以违反第二个条件。

我一直认为纯净的两个条件是相辅相成的:

  • 结果评估不得影响副状态
  • 评估结果不得旁态影响

术语“副作用”仅指第一种,该功能修改了非局部状态。但是,有时读操作也被视为副作用:当它们是操作且也涉及写入时,即使其主要目的是访问值。例如,生成伪随机数来修改生成器的内部状态,从推进读取位置的输入流中读取数据,或者从涉及“获取测量”命令的外部传感器读取数据。


1
谢谢贝尔吉。出于某种原因,我认为副作用包括读取局部作用域之外的变量,但是我猜想,如果它写入此类外部变量,那只是副作用。
Magnus

17
如果prompt("you choose")没有副作用,我们应该退后一步,阐明副作用的含义。
Holger

1
@Magnus是的,正是效果的意思。我也会尝试在答案中加以澄清,没想到会引起如此多的关注,并希望做出值得数十票赞成的答案:-)
Bergi

2
好吧,您知道吗Math.random()返回一个热敏二极管。实际未指定使用错误的RNG。
约书亚

1
在这两种情况中,我听说前者称为“效应”,而后者称为“协同效应”。两者都是“副作用”和不纯的。f(coeffects,input)->效果,输出Coeffects是来自更广泛的环境变化的输入,是可以改变更广阔的环境的输出。例如,Elm和Clojurescrips重新设计了该模型。

30

表述纯函数是什么的“正常”方式是参照透明性。如果函数是参照透明的,则它是函数。

大致来说,参照透明性是指您可以在程序中的任何位置用其返回值替换对函数的调用,反之亦然,而无需更改程序的含义。

因此,例如,如果Cprintf是参照透明的,则这两个程序应具有相同的含义:

printf("Hello");

5;

并且以下所有程序应具有相同的含义:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

因为printf返回写入的字符数,在这种情况下为5。

void功能变得更加明显。如果我有功能void foo,那么

foo(bar, baz, quux);

应该与

;

即既然foo什么都不返回,那么我应该能够在不改变程序含义的情况下将其替换为什么。

显然,然后,既不printf也不foo是引用透明,因此两者都不是纯的。实际上,void除非是非操作函数,否则它永远不可能是参照透明的。

我发现这个定义比您给的定义更容易处理。它还允许您以任意粒度应用它:可以将其应用到单个表达式,函数以及整个程序。例如,它允许您谈论这样的功能:

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

我们可以分析组成函数的表达式,并容易得出结论,它们不是参照透明的,因此不是纯净的,因为它们使用可变的数据结构,即memo数组。但是,我们还可以查看该函数,并且可以看到它参照透明的,因此是纯函数。有时将其称为外部纯净,即,对外部世界看似纯净的功能,但在内部却不纯净地实现。

这样的功能仍然有用,因为当杂质感染周围的所有东西时,外部纯接口会建立一种“纯度屏障”,其中杂质只会感染功能的三行,而不会泄漏到程序的其余部分。这三行比整个程序更容易分析正确性。


2
一旦并发,该杂质会影响整个程序。
R .. GitHub停止帮助ICE,

@R ..您能想到一种并发使所描述的斐波那契函数在外部不纯的方法吗?我不能 写入memo[n]是幂等的,而从中读取数据只会浪费CPU周期。
Brilliand

我同意你们俩 杂质导致并发问题,但在这种特定情况下却不会。
约尔格W¯¯米塔格

@R ..不难想象并发感知版本。
user253751 '19

1
@Brilliand例如,memo[n] = ...可以首先创建一个字典条目,然后将值存储到其中。这就留下了一个窗口,在此期间另一个线程可以看到未初始化的条目。
user253751

12

在我看来,您描述的第二个条件比第一个条件弱。

让我举一个例子,假设您有一个函数可以添加一个也记录到控制台的函数:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

您提供的第二个条件得到满足:给定相同的输入时,此函数始终返回相同的输出。但是,它不是纯粹的功能,因为它包括登录到控制台的副作用。

严格来讲,纯函数是满足参照透明性的函数。这就是我们可以用其产生的值替换功能应用程序的属性,而无需更改程序的行为。

假设我们有一个简单地添加以下功能的函数:

function addOne(x) {
  return x + 1;
}

我们可以在程序中的任何地方替换它addOne(5)6并且什么都不会改变。

相比之下,我们不能在不更改行为的情况下用程序中的任何位置替换addOneAndLog(x)6,因为第一个表达式导致某些内容被写入控制台,而第二个表达式则没有。

我们认为addOneAndLog(x)除了返回输出以外,任何这种额外的行为都会产生副作用


“在我看来,您描述的第二个条件比第一个条件更弱。” 不,这两个条件在逻辑上是独立的。
sleske

@sleske,您弄错了。我已经为术语“纯”和“副作用”提供了清晰的定义。在这些约束条件下,除了给定输入返回相同的输出外,没有任何副作用的函数。但是,我提供了一些示例,其中没有第一个条件就可以满足第二个条件。理解纯净概念的基本原理是指透明性。
TheInnerLight

小错字:没有什么,有没有副作用的函数可以做,除了返回相同的输出给定的输入。
TheInnerLight

比如返回当前时间呢?这没有副作用,但是对于相同的输入确实会返回不同的输出。或更一般而言,结果不仅仅取决于输入参数,还取决于(可变)全局变量的任何函数。
sleske

2
似乎您使用的“副作用”定义与常用定义不同。副作用通常被定义为“除返回值外还具有可观察到的效果”或“状态可观察到的变化”-例如,参见Wikipedia该文章在softwareengineering.SE上。您是完全正确的,Date.now()它不是纯粹/相对透明的,但这不是因为它具有副作用,而是因为其结果不仅取决于其输入。
sleske

7

系统外部可能存在随机性。假设计算的一部分包括室温。然后,根据室温的随机外部因素,每次执行该功能都会产生不同的结果。通过执行程序不会更改状态。

无论如何,我所能想到的。


3
据我说,这些“来自系统外部的随机性”是一种副作用。具有这些行为的功能不是“钱包”。
约瑟夫·狄翁

2

FP定义的问题在于它们是非常人为的。每次评估/计算都会对评估者产生副作用。从理论上讲是正确的。对此的否认仅表明FP辩护者无视哲学和逻辑:“评估”意味着改变某些智能环境(机器,大脑等)的状态。这就是评估过程的本质。没有变化-没有“结石”。效果非常明显:加热CPU或其故障,以防过热而关闭主板,等等。

当您谈论引用透明性时,您应该理解有关透明性的信息对于整个系统的创建者和语义信息的持有者都是人类可用的,而编译器可能不可用。例如,一个函数可以读取一些外部资源,并且其签名中将包含IO monad,但始终会返回相同的值(例如的结果current_year > 0)。编译器不知道该函数将始终返回相同的结果,因此该函数不纯,但具有参照透明属性,可以用True常量替换。

因此,为避免这种不准确性,我们应该区分数学函数和编程语言中的“函数”。Haskell中的函数始终是不纯的,与它们相关的纯度的定义始终是有条件的:它们在具有真实副作用和物理特性的真实硬件上运行,这对于数学函数是错误的。这意味着带有“ printf”功能的示例是完全错误的。

但是并不是所有的数学函数也都是纯函数:每个以t(时间)为参数的函数可能都是不纯净的:t具有函数的所有作用和随机性:在通常情况下,您有输入信号,却不知道实际值,甚至成为噪音。


2

如果第一个条件始终为真,那么是否有第二次条件不为真?

考虑下面的简单代码段

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

对于相同的给定输入集,此代码将返回随机输出-但是它没有任何副作用。

您提到的两个点#1和#2的总体效果是:在任何时候,如果将Sum具有相同i / p的函数替换为程序中的结果,则程序的整体含义不变。这不过是参照透明


但是在这种情况下,第一个条件未被验证:写入控制台被认为是副作用,因为它会更改计算机本身的状态。
右腿

@Rightleg thx指出。我以某种方式完全误解了OP。更正的答案。
rahulaga_dev

2
它不会改变随机发生器的状态吗?
Eric Duminil

1
生成随机数本身是一个副作用,除非随机数发生器的状态被显式提供这将使该函数满足条件2
TheInnerLight

1
rnd不会转义该函数,因此其状态改变的事实与该函数的纯度无关紧要,但是Random构造函数使用当前时间作为种子值的事实意味着除a和之外还有“输入” b
Sneftel
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.