将函数定义为pure
的两个条件如下:
- 无副作用(即只允许更改本地范围)
- 给定相同的输入,始终返回相同的输出
如果第一个条件总是为真,那么是否有第二个条件不为真的时候?
即真的只需要第一个条件吗?
以下是一些反例,它们不会改变外部范围,但仍被认为是不纯的:
function a() { return Date.now(); }
function b() { return window.globalMutableVar; }
function c() { return document.getElementById("myInput").value; }
function d() { return Math.random(); }
(诚然确实改变了PRNG,但不被认为是可观察的)
访问非常量非局部变量足以违反第二个条件。
我一直认为纯洁的两个条件是互补的:
- 结果评估不得对侧面状态产生影响
- 评估结果不得受侧面状态影响
术语副作用仅指第一个,即修改非局部状态的函数。但是,有时读取操作也被视为副作用:当它们是操作并且也涉及写入时,即使它们的主要目的是访问值。这方面的示例包括生成修改生成器内部状态的伪随机数,从推进读取位置的输入流中读取,或从涉及"进行测量"命令的外部传感器读取。
表达纯函数的"正常"方式是参照透明度。如果函数在引用上是透明的,则该函数是纯函数。
引用透明度大致意味着您可以在程序中的任何点将函数的调用替换为其返回值,反之亦然,而无需更改程序的含义。
因此,例如,如果 C 的printf
在引用上是透明的,则这两个程序应该具有相同的含义:
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
数组。但是,我们也可以看到该函数,可以看到它是引用透明的,因此是纯粹的。这有时被称为外部纯度,即在外部世界看来是纯洁的函数,但在内部实现却不纯洁。
这样的函数还是有用的,因为虽然杂质会感染周围的一切,但外部纯接口会构建一种"纯度屏障",其中杂质只感染函数的三行,但不会泄漏到程序的其余部分。这三行比整个程序更容易分析正确性。
在我看来,您描述的第二个条件比第一个条件更弱。
让我举个例子,假设你有一个函数来添加一个也记录到控制台的函数:
function addOneAndLog(x) {
console.log(x);
return x + 1;
}
您提供的第二个条件是满足的:当给定相同的输入时,此函数始终返回相同的输出。 但是,它不是一个纯函数,因为它包含记录到控制台的副作用。
严格来说,纯函数是满足参照透明度属性的函数。 这就是我们可以在不更改程序行为的情况下用函数应用程序产生的值替换函数应用程序的属性。
假设我们有一个函数,它只是添加:
function addOne(x) {
return x + 1;
}
我们可以在程序中的任何位置用6
替换addOne(5)
,并且不会有任何变化。
相比之下,我们不能在不更改行为的情况下将addOneAndLog(x)
替换为程序中任何地方6
的值,因为第一个表达式会导致某些内容被写入控制台,而第二个表达式则不会。
我们将addOneAndLog(x)
除了返回输出之外执行的任何这种额外行为视为副作用。
可能存在来自系统外部的随机性来源。假设计算的一部分包括室温。然后执行该函数每次都会产生不同的结果,具体取决于室温的随机外部元素。执行程序不会更改状态。
无论如何,我能想到的都是。
FP定义的问题在于它们非常人为。每次评估/计算都会对评估者产生副作用。这在理论上是正确的。否认这一点只表明FP辩护者忽略了哲学和逻辑:"评估"意味着改变某些智能环境(机器,大脑等)的状态。这就是评估过程的本质。没有变化 - 没有"微积分"。效果可能非常明显:加热 CPU 或其故障,在过热的情况下关闭主板等等。
当你谈论引用透明度时,你应该明白,关于这种透明度的信息对于作为整个系统的创建者和语义信息的持有者是可用的,而编译器可能无法获得。例如,一个函数可以读取一些外部资源,它的签名中会有 IO monad,但它会一直返回相同的值(例如,current_year > 0
的结果)。编译器不知道函数将始终返回相同的结果,因此该函数是不纯的,但具有引用透明的属性,可以用常量替换True
。
因此,为了避免这种不准确性,我们应该区分编程语言中的数学函数和"函数"。Haskell中的函数总是不纯的,与它们相关的纯度定义总是非常有条件的:它们运行在具有真实副作用和物理属性的真实硬件上,这对于数学函数来说是错误的。这意味着带有"printf"函数的示例是完全不正确的。
但并非所有的数学函数都是纯粹的:每个以t
(时间)作为参数的函数可能是不纯的:t
包含函数的所有效应和随机性质:在通常情况下,你有输入信号并且不知道实际值,它甚至可能是一个噪声。
如果第一个条件总是为真,那么第二个条件是否为真 条件不对?
是的
考虑下面的简单代码片段
public int Sum(int a, int b) {
Random rnd = new Random();
return rnd.Next(1, 10);
}
此代码将返回同一组给定输入的随机输出 - 但它没有任何副作用。
你提到的#1和#2点组合在一起时的总体效果意味着:在任何时间点,如果具有相同i/p的函数Sum
被替换为程序中的结果,程序的整体含义不会改变。这只不过是参考透明度。