【问题标题】:Is this JavaScript function, taking a mutable reference argument, a pure function?这个带有可变引用参数的 JavaScript 函数是纯函数吗?
【发布时间】:2021-01-24 15:32:41
【问题描述】:

我和this one 有同样的问题,但在 JavaScript 的上下文中。

来自Wikipedia:

[一个纯函数的]返回值对于 same 参数是相同的

那里进一步声称,纯函数不允许使用“可变引用参数”来改变返回值。在 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 的那一刻到完成执行时将保持不变。 (如果我理解正确,这种解释似乎得到了Verifiable Functional Purity in Java § 4.1 中提出的“相同”定义的支持。)

或者我错过了什么? JavaScript 中是否有一个示例,其中不包含异步代码的函数会丢失 referential transparency 的属性,仅仅是因为它采用可变引用,但如果我们使用例如取而代之的是 Immutable.js 数据结构?

【问题讨论】:

  • 在 Javascript 中,对象通过它们的引用获得身份。这种引用身份属性与纯功能语言中的引用透明性相反。如此正式地,Javascript 中没有纯函数,但您可以通过约定和信心获得所需的行为。
  • 来自论文:“对于这些问题,没有一个明显正确的答案。因此,确定性是一个参数化的属性:给定参数等价的含义的定义,如果所有带有等效参数的调用返回的结果与该语言内无法区分,则该方法是确定性的”。
  • 识别纯函数的原因之一是删除对它们的不必要调用或重新排序代码。您必须考虑两个 x 不同,才能将函数视为纯函数。如果这就是分析仪所做的,那很好。但如果没有这个上下文,这个函数就不是纯粹的。
  • 所以这不是见仁见智吗?该论文本身表明,对等的定义是一种选择(在一定范围内)。那你是在问一个选择吗?我想这会导致固执己见的答案。
  • Redux 开发人员会认为f = () => [] 是“纯”的,即使f() === f() 是假的,因为[] === [] 是假的。因此,您绝对必须在您将如何使用它们的上下文中定义事物等价的含义。还要考虑如果您只是将突变作为属性获取器烘焙会发生什么:x = { get length() { return global++; } } 然后,通过预先将global 设置为1,f(x) == f(x) 很容易为假。所以arr => arr.length和g => g()一样纯粹

标签: javascript function functional-programming purely-functional


【解决方案1】:

当使用Wikipedia definition 时,将可变数据结构(例如本机数组)的引用作为参数的函数不是纯函数:

对于相同的参数(与局部静态变量、非局部变量、可变引用参数或来自 I/O 的输入流,其返回值相同设备)。

等价

虽然这清楚地表明“没有可变引用参数的变化”,但我们可以说这是可以解释的,并且取决于 “相同” 和 “变化”的含义时间>。可能有不同的定义,所以我们进入意见领域。引自您提到的论文:

对于这些问题,没有一个明显正确的答案。因此,确定性是一个参数化的属性:给定参数等效的定义,如果所有使用 equivalent 参数的调用返回的结果与语言内

在同一篇论文中提出的功能纯度,使用了以下等价定义:

如果两组对象引用产生相同的对象图,则它们被认为是等效的

因此,根据该定义,以下两个数组被认为是等效的:

let a = [1];
let b = [1];

但是如果不增加更多限制,这个概念不能真正应用于 JavaScript。也不是 Java,这也是本文作者提到一种精简的语言 Joe-E 的原因:

对象有身份:从概念上讲,它们有一个“地址”,我们可以使用== 运算符比较两个对象引用是否指向同一个“地址”。这种对象身份的概念可能会暴露不确定性。

用 JavaScript 说明:

const compare = (array1, array2) => array1 === array2;

let arr = [1];
let a = compare(arr, arr);
let b = compare(arr, [1]);
console.log(a === b); // false

由于两个调用返回不同的结果,即使参数具有相同的形状和内容,我们应该得出结论(使用等价的定义)上述函数compare 不是纯函数。在 Java 中,您可以影响 == 运算符的行为(Joe-E 禁止调用 Object.hashCode),因此避免这种情况发生,这在 JavaScript 中比较对象时通常是不可能的。

意外的副作用

另一个问题是 JavaScript 不是强类型的,因此函数不能确定它接收到的参数是他们想要的。例如,下面的函数看起来很纯粹:

const add = (a, b) => a + b;

但调用方式可以产生副作用:

const add = (a, b) => a + b;

let i = 0;
let obj = { valueOf() { return i++ } };
let a = add(1, obj);
let b = add(1, obj);
console.log(a === b); // false

您问题中的函数也存在同样的问题:

const f = (arr) => arr.length;

const x = { get length() { return Math.random() } };
let a = f(x);
let b = f(x);
console.log(a === b) // false

在这两种情况下,函数都无意中调用了不纯函数并返回了依赖于它的结果。虽然在第一个示例中,使用typeof 检查仍然很容易使函数成为纯函数,但这对您的函数来说并不那么微不足道。我们可以想到instanceof 或Array.isArray,甚至是一些智能的deepCompare 函数,但调用者仍然可以设置一个奇怪对象的原型,设置其构造函数属性,用getter 替换原始属性,将对象包装在代理中, ......等等等等,甚至连最聪明的平等检查员也能愚弄。

实用主义

在 JavaScript 中,“松散的结局”太多了,为了对“纯”有一个有用的定义,必须务实,否则几乎没有什么可以被标记为纯。

例如,实际上很多人会调用像Array#slicepure 这样的函数,即使它存在上述问题(包括与特殊参数this 相关的问题)。

结论

在 JavaScript 中,当调用纯函数时,您通常必须就如何调用函数达成一致。参数应该是某种类型,并且没有(隐藏的)可以调用但不纯的方法。

有人可能会争辩说,这违背了“纯”背后的理念,它应该只由函数定义本身决定,而不是最终可能被调用的方式。

【讨论】:

  • 谢谢! compare 函数的良好演示。我想在实践中,我可以称它为不纯的......
  • 您引用的维基百科文章已经指出:“一些作者,特别是来自命令式语言社区的作者,对所有仅具有上述属性 2 的函数使用术语“纯” ",还是很有用的。因此,只要您不改变可变参数(并且没有其他人这样做),您的函数仍然是纯的。
  • 还添加了引用“可变引用参数”的部分in these edits,并且没有来自literate的引用支持。还有quite some discussion on the talk page。
猜你喜欢
  • 1970-01-01
  • 2018-08-05
  • 1970-01-01
  • 2021-05-16
  • 2021-06-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-05
相关资源
最近更新 更多