85

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

  1. 没有副作用(即只允许更改本地范围)
  2. 给定相同的输入,总是返回相同的输出

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

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

4

6 回答 6

115

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

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

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

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

  • 结果评估不得对副状态产生影响
  • 评估结果必须不受side state的影响

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

于 2019-03-04T22:10:12.247 回答
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数组。但是,我们也可以查看该函数,并且可以看到它引用透明的,因此是纯的。这有时被称为外部纯度,即在外界看来是纯粹的,但在内部实现是不纯粹的。

这样的函数还是有用的,因为杂质感染了它周围的一切,外部的纯接口构建了一种“纯度屏障”,杂质只感染函数的三行,但不会泄漏到程序的其余部分. 这三行比整个程序更容易分析正确性。

于 2019-03-05T06:32:49.230 回答
12

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

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

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

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

严格来说,纯函数是一个满足引用透明性的函数。这是我们可以在不改变程序行为的情况下用它产生的值替换函数应用程序的属性。

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

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

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

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

我们认为addOneAndLog(x)除了返回输出之外的任何这种额外行为都是副作用

于 2019-03-04T23:03:18.527 回答
6

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

反正我能想到的。

于 2019-03-04T22:11:14.837 回答
2

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

是的

考虑下面的简单代码片段

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

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

您提到的第 1 点和第 2 点结合在一起时的整体效果意味着:在任何时间点,如果Sum在程序中将具有相同 i/p 的函数替换为其结果,则程序的整体含义不会改变。这不过是参照透明性

于 2019-03-05T07:27:05.423 回答
2

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

当您谈论引用透明度时,您应该了解有关这种透明度的信息可供作为整个系统的创建者和语义信息持有者的人类使用,而编译器可能无法使用。例如,一个函数可以读取一些外部资源,它的签名中有 IO monad,但它会始终返回相同的值(例如,的结果current_year > 0)。编译器不知道函数总是返回相同的结果,因此该函数是不纯的,但具有引用透明的属性,可以用True常量代替。

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

但并非所有数学函数都是纯的:每个以t(时间)为参数的函数可能是不纯的:t包含函数的所有效果和随机性质:通常情况下,您有输入信号并且不知道实际值,它可以甚至是噪音。

于 2019-03-08T08:53:35.287 回答