纯函数 return 可以是 Symbol 吗?

Can a pure function return a Symbol?

这可能有点哲学意义,但我认为这是提问的正确地点。

假设我有一个创建 ID 列表的函数。这些标识符仅在应用程序内部使用,因此可以在此处使用 ES2015 Symbol()

我的问题是,技术上,当你要求一个 Symbol 时,我想 JS 运行时会创建一个唯一标识符(随机数?内存地址?不确定) ,为了防止碰撞,需要访问全局状态。我不确定的原因是因为那个词,"technically"。我不确定(同样,从哲学的角度来看)这是否足以打破 API 呈现的数学抽象。

tl;dr: 这是一个例子--

function sentinelToSymbol(x) {
  if (x === -1) return Symbol();
  return x;
}

这个函数是纯函数吗?

不是真的,不是,但实际上可能并不重要。

表面上,(foo) => Symbol(foo)显得很纯洁。虽然运行时 可能 执行一些具有副作用的操作,但您永远不会看到它们,即使您同时使用相同的参数调用 Symbol() 也是如此。但是,使用相同的参数调用 Symbol 永远不会 return 相同的值,这是主要标准之一(下面的#2)。

来自 the MDN page:

Note that Symbol("foo") does not coerce the string "foo" into a symbol. It creates a new symbol each time:

Symbol("foo") === Symbol("foo"); // false

只看副作用,(foo) => Symbol(foo)是纯粹的(在运行时之上)。

但是,纯函数必须满足更多条件。 From Wikipedia:

Purely functional functions (or expressions) have no side effects (memory or I/O). This means that pure functions have several useful properties, many of which can be used to optimize the code:

  • If the result of a pure expression is not used, it can be removed without affecting other expressions.
  • If a pure function is called with arguments that cause no side-effects, the result is constant with respect to that argument list (sometimes called referential transparency), i.e. if the pure function is again called with the same arguments, the same result will be returned (this can enable caching optimizations such as memoization).
  • If there is no data dependency between two pure expressions, then their order can be reversed, or they can be performed in parallel and they cannot interfere with one another (in other terms, the evaluation of any pure expression is thread-safe).
  • If the entire language does not allow side-effects, then any evaluation strategy can be used; this gives the compiler freedom to reorder or combine the evaluation of expressions in a program (for example, using deforestation).

您可能会争辩说该列表的前言排除了 JavaScript 中的 一切 ,因为任何操作都可能导致分配内存、更新内部结构等。在最严格的解释,JS 从来都不是纯粹的。这不是很有趣或有用,所以...

此功能符合标准 #1。忽略结果,(foo) => Symbol(foo)(foo) => () 与任何外部观察者都相同。

标准#2 给我们带来了更多麻烦。鉴于 bar = (foo) => Symbol(foo)bar('xyz') !== bar('xyz'),所以 Symbol 根本不满足该要求。每次调用 Symbol.

时,您保证会得到一个唯一的实例

继续,标准 #3 没有问题。您可以从不同的线程调用 Symbol 而不会发生冲突(并行),并且它们的调用顺序无关紧要。

最后,标准 #4 与其说是直接要求,不如说是一个注释,而且很容易满足(JS 运行时会随机播放所有内容)。

因此:

  • 严格来说,JS中没有什么是纯粹的。
  • Symbol() 绝对不是纯粹的,因此这个例子也不是。
  • 如果您只关心副作用而不是记忆,那么该示例确实符合这些标准。

是的,这个函数是不纯的:sentinelToSymbol(-1) !== sentinelToSymbol(-1)。我们希望这里的纯函数相等。

但是,如果我们在具有对象标识的语言中使用引用透明性的概念,我们可能希望稍微放宽我们的定义。如果考虑function x() { return []; },是不是很纯粹?显然 x() !== x(),但无论输入如何,函数始终 returns 是一个空数组,就像常量函数一样。所以我们在这里必须定义的是我们语言中值的相等性。 === 运算符可能不是这里的最佳选择(只需考虑 NaN)。如果包含相同的元素,数组是否彼此相等?可能是的,除非它们在某处发生了变异。

所以你现在必须为你的符号回答同样的问题。符号是不可变的,这使得这部分变得容易。现在我们可以认为它们的 [[Description]] 值(或 .toString())相等,因此 sentinelToSymbol 根据该定义是纯的。

但大多数语言确实具有允许打破引用透明性的函数 - 例如参见 [​​=18=]。在 JavaScript 中,这将在其他方面相等的对象上使用 ===。它将使用符号作为属性,因为这会检查它们的身份。因此,如果您不在您的程序中使用此类操作(或者至少不被外部观察到),您可以声明您的函数的纯度并将其用于对您的程序进行推理。