纯函数:"No Side Effects" 是否意味着 "Always Same Output, Given Same Input"?

Pure Functions: Does "No Side Effects" Imply "Always Same Output, Given Same Input"?

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

  1. 无副作用(即只允许更改局部范围)
  2. 总是return相同的输出,给定相同的输入

如果第一个条件总是为真,是否有任何时候第二个条件不为真?

即真的只需要满足第一个条件吗?

下面是几个不改变外作用域但仍然被认为是不纯的反例:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); }(这确实改变了 PRNG,但不被认为是可观察的)

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

我一直认为纯度的两个条件是互补的:

  • 结果评估不得对边状态产生影响
  • 评估结果不得受到旁路状态的影响

术语side effect仅指第一个,修改非局部状态的函数。然而,有时读取操作也被认为是副作用:当它们是 操作 并且也涉及写入时,即使它们的主要目的是访问一个值。这方面的例子是生成一个伪随机数来修改生成器的内部状态,从输入流中读取使读取位置前进,或者从涉及 "take measurement" 命令的外部传感器读取。

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

反正我能想到的就这些了

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

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

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

您提供的第二个条件得到满足:此函数在给定相同输入时总是returns相同的输出。但是,它不是一个纯函数,因为它包含将日志记录到控制台的副作用。

纯函数严格来说就是满足referential transparency的属性的函数。那就是 属性 我们可以在不改变程序行为的情况下用它产生的值替换函数应用程序。

假设我们有一个简单添加的函数:

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

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

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

我们认为 addOneAndLog(x) 除了将输出作为 副作用 .

执行的任何额外行为

"normal" 表述 纯函数 的方式是根据 参照透明性 。如果函数引用透明

,则函数是

Referential Transparency,粗略地说,您可以在程序中的任何位置用它的 return 值替换对函数的调用,反之亦然,而无需更改程序的意义。

因此,例如,如果 C 的 printf 是引用透明的,那么这两个程序应该具有相同的含义:

printf("Hello");

5;

和以下所有程序应该具有相同的含义:

5 + 5;

printf("Hello") + 5;

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

因为printf returns写入的字符数,本例为5.

使用 void 函数会变得更加明显。如果我有一个函数 void foo,那么

foo(bar, baz, quux);

应该和

一样
;

即因为 foo return 什么都没有,我应该可以在不改变程序含义的情况下用任何东西替换它。

很明显,printffoo 都不是引用透明的,因此它们都不是纯粹的。事实上,一个 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 数组。但是,我们也可以查看该函数,可以看到它 引用透明的,因此是纯粹的。这有时被称为外部纯度,即一个函数在外部世界看来是纯净的,但在内部实现是不纯净的。

这样的函数还是有用的,因为当杂质感染周围的一切时,外部纯接口构建了一种"purity barrier",其中杂质只感染函数的三行,但不会泄漏进入程序的其余部分。这三行比整个程序更容易分析正确性。

If the first condition is always true, are there any times the second condition is not true?

考虑下面的简单代码片段

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

此代码将 return 为相同的给定输入集随机输出 - 但是它没有任何副作用。

您提到的第 1 点和第 2 点结合在一起的整体效果意味着:在任何时间点,如果函数 Sum 与相同的 i/p 被替换其结果在一个程序中,程序的整体意义没有改变。这只不过是 Referential transparency.

FP 定义的问题在于它们非常人为。每个 evaluation/calculation 对评估者都有副作用。这在理论上是正确的。否认这一点仅表明 FP 辩护者忽视了哲学和逻辑:"evaluation" 意味着某些智能环境(机器、大脑等)状态的改变。这是评估过程的本质。没有变化 - 没有 "calculi"。效果非常明显:加热 CPU 或其故障,在过热的情况下关闭主板,等等。

当你谈到引用透明时,你应该明白,关于这种透明的信息对于作为整个系统的创建者和语义信息的持有者的人类是可用的,而对于编译器可能是不可用的。例如,一个函数可以读取一些外部资源,它的签名中会有 IO monad,但它始终 return 相同的值(例如, current_year > 0 的结果)。编译器不知道函数将 return 总是相同的结果,因此该函数是不纯的但具有引用透明 属性 并且可以用 True 常量替换。

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

但并非所有数学函数都是纯的:每个以 t(时间)作为参数的函数可能是不纯的:t 包含函数的所有效应和随机性:共同点如果您有输入信号但不知道实际值,它甚至可能是噪音。