这可能近乎哲学,但我认为这是一个正确的问题。
假设我有一个创建ID列表的函数。这些标识符仅在应用程序内部使用,因此可以在此处使用ES2015 Symbol()
。
我的问题是,技术上,当你要求一个Symbol时,我想JS运行时会创建一个唯一的标识符(随机数?内存地址?不确定),为了防止冲突,需要访问全局状态。我之所以不确定是因为"技术上"这个词。我不确定(同样,从哲学的角度来看)这是否足以打破API提出的数学抽象。
tl;dr:这里有一个例子——
function sentinelToSymbol(x) {
if (x === -1) return Symbol();
return x;
}
这个函数是纯函数吗
不是,不是,但实际上可能无关紧要。
表面上看,(foo) => Symbol(foo)
是纯净的。虽然运行时可能会执行一些有副作用的操作,但您永远不会看到它们,即使您使用相同的参数同时调用Symbol()
也是如此。但是,使用相同的参数调用Symbol
永远不会返回相同的值,这是主要标准之一(下面的#2)。
从MDN页面:
请注意,Symbol("foo")不会将字符串"foo"强制转换为符号。它每次都会创建一个新符号:
Symbol("foo") === Symbol("foo"); // false
仅从副作用来看,(foo) => Symbol(foo)
是纯的(在运行时之上)。
然而,一个纯函数必须满足更多的标准。来自维基百科:
纯粹的函数(或表达式)没有副作用(内存或I/O)。这意味着纯函数具有几个有用的属性,其中许多可以用于优化代码:
- 如果不使用纯表达式的结果,则可以在不影响其他表达式的情况下将其删除
- 如果使用不会产生副作用的参数调用纯函数,则结果相对于该参数列表是恒定的(有时称为引用透明度),即如果再次使用相同的参数调用该纯函数,将返回相同的结果(这可以启用缓存优化,如内存化)
- 如果两个纯表达式之间没有数据依赖关系,那么它们的顺序可以颠倒,或者可以并行执行,并且它们不能相互干扰(换句话说,任何纯表达式的求值都是线程安全的)
- 如果整个语言不允许副作用,那么可以使用任何评估策略;这使编译器可以自由地重新排序或组合程序中表达式的求值(例如,使用毁林)
你可以争辩说,该列表的序言排除了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()
,但函数仍然总是返回一个空数组,而不管输入是什么,就像一个常量函数一样。因此,我们必须在这里定义的是我们语言中价值观的平等。===
运算符在这里可能不是最合适的(只考虑NaN
)。如果包含相同的元素,数组是否相等?可能是的,除非它们在某个地方发生了变异。
所以你现在必须为你的符号回答同样的问题。符号是不可变的,这使得这个部分变得容易。现在我们可以认为它们的[[Description]]值(或.toString()
)相等,因此根据该定义,sentinelToSymbol
将是纯的。
但大多数语言都有允许打破引用透明性的函数——例如,请参阅Haskell中如何打印列表的内存地址。在JavaScript中,这将在其他相等的对象上使用===
。它将使用符号作为属性,以检查它们的身份。因此,如果你在程序中不使用这样的操作(或者至少在不被外界观察到的情况下),你可以声称你的函数是纯净的,并用它来推理你的程序。