这个带有可变引用参数的JavaScript函数是一个纯函数吗?
我和这个有同样的问题,但在 JavaScript 的上下文中。
来自维基百科:
[纯函数的] 返回值对于相同的参数是相同的
那里进一步声称,纯函数不允许具有“可变引用参数”的返回值变化。在 JavaScript 中,每个普通对象都作为“可变引用参数”传递。考虑以下示例:
const f = (arr) => arr.length
const x = []
console.log( f(x) ) // 0
x.push(1);
console.log( f(x) ) // 1
上面的证明f是不纯的吗?或者你会争辩说我们f在这两种情况下没有用“相同”的参数调用?
我可以看到f在其他线程可能会在f执行时混淆可变引用参数的语言/环境中调用不纯是如何有意义的。但既然f不是async,就不可能发生这种情况。x从f调用的那一刻到执行完成,都将保持不变。(如果我理解正确,这种解释似乎得到了Java中可验证功能纯度的第 4.1 节中提出的“相同”定义的支持。)
或者我错过了什么?在 JavaScript 中是否有一个例子,其中一个不包含异步代码的函数仅仅因为它采用了可变引用而失去了引用透明性的属性,但如果我们使用例如 Immutable.js 数据结构来代替它,它会是纯粹的吗?
回答
当采用Wikipedia 定义时,将可变数据结构(例如本机数组)的引用作为参数的函数不是纯函数:
对于相同的参数,它的返回值是相同的(局部静态变量、非局部变量、可变引用参数或来自 I/O 设备的输入流没有变化)。
等价
虽然这清楚地说“没有可变引用参数的变化”,但我们可以说这是可以解释的,取决于“相同”和“变化”的含义。可能有不同的定义,因此我们进入意见领域。引用自您所指的论文:
这些问题没有一个明显正确的答案。因此,确定性是一个参数化属性:给定参数等效含义的定义,如果所有具有等效参数的调用返回的结果与语言内部无法区分,则该方法是确定性的
同一篇论文中提出的功能纯度使用以下等效定义:
如果两组对象引用产生相同的对象图,则认为它们是等效的
因此,根据该定义,以下两个数组被认为是等效的:
let a = [1];
let b = [1];
let a = [1];
let b = [1];
但是这个概念不能真正应用于 JavaScript 而不添加更多限制。也不是 Java,这就是本文作者提到一种名为 Joe-E 的精简语言的原因:
对象有身份:从概念上讲,它们有一个“地址”,我们可以使用
==运算符比较两个对象引用是否指向同一个“地址” 。这种对象身份的概念可能会暴露出不确定性。
用 JavaScript 说明:
由于这两个调用返回不同的结果,即使参数具有相同的形状和内容,我们也应该得出结论(根据这个等价的定义)上述函数compare不是纯函数。虽然在 Java 中您可以影响==操作符的行为(Joe-E 禁止调用Object.hashCode),从而避免这种情况发生,但在比较对象时,这在 JavaScript 中通常是不可能的。
意想不到的副作用
另一个问题是 JavaScript 不是强类型的,因此函数无法确定它接收到的参数是否符合预期。例如,以下函数看起来很纯:
const add = (a, b) => a + b;
const add = (a, b) => a + b;
但是可以通过调用它来产生副作用:
const compare = (array1, array2) => array1 === array2;
let arr = [1];
let a = compare(arr, arr);
let b = compare(arr, [1]);
console.log(a === b); // false
您问题中的函数存在同样的问题:
在这两种情况下,函数都无意中调用了一个不纯函数并返回了一个依赖于它的结果。虽然在第一个示例中仍然很容易通过typeof检查使函数成为纯函数,但这对您的函数来说不是那么简单。我们可以想到instanceofor Array.isArray,甚至一些智能deepCompare函数,但是,调用者仍然可以设置一个奇怪对象的原型,设置它的构造函数属性,用 getter 替换原始属性,将对象包装在代理中,等等,等等即使是最聪明的平等检查员也能愚弄。
实用主义
由于在 JavaScript 中有太多“松散的结局”,因此必须务实才能对“纯”有一个有用的定义,否则几乎没有什么可以标记为纯的。
例如,在实践中,许多人会调用像Array#slicepure这样的函数,即使它存在上述问题(包括与特殊参数相关的问题this)。
结论
在 JavaScript 中,当调用纯函数时,您通常必须就如何调用函数达成一致。参数应该是某种类型,并且没有可以调用但不纯的(隐藏)方法。
有人可能会争辩说,这与“纯”背后的想法背道而驰,“纯”应该只由函数定义本身决定,而不是最终可能被调用的方式。