从技术上讲,您在计算机上执行的任何程序都是不纯的,因为它最终会编译为诸如“将此值移入”和“将此值添加到”之类的指令,这些指令是不纯的。这不是很有帮助。eax
eax
相反,我们使用黑匣子来考虑纯度。如果某些代码在给定相同的输入时总是产生相同的输出,那么它被认为是纯的。根据此定义,即使内部它使用不纯的 memo 表,以下函数也是纯函数。
const fib = (() => {
const memo = [0, 1];
return n => {
if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
};
})();
console.log(fib(100));
我们不关心内部,因为我们使用黑匣子方法来检查纯度。同样,我们并不关心所有代码最终都转换为不纯的机器指令,因为我们正在考虑使用黑匣子方法的纯度。内部不重要。
现在,考虑以下函数。
const greet = name => {
console.log("Hello %s!", name);
};
greet("World");
greet("Snowman");
函数是纯的还是不纯的?根据我们的黑匣子方法,如果我们给它相同的输入(例如),那么它总是将相同的输出打印到屏幕上(即)。从这个意义上说,它不是纯粹的吗?不,它不是。它不纯净的原因是,我们认为将某些东西打印到屏幕上是一种副作用。如果我们的黑匣子产生副作用,那么它就不是纯粹的。greet
World
Hello World!
什么是副作用?这就是参照透明度概念有用的地方。如果一个函数在引用上是透明的,那么我们总是可以用它们的结果替换该函数的应用程序。请注意,这与函数内联不同。
在函数内联中,我们将函数的应用程序替换为函数的主体,而不改变程序的语义。但是,引用透明函数始终可以替换为其返回值,而不会更改程序的语义。请考虑以下示例。
console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");
在这里,我们内联了的定义,并且它没有改变程序的语义。greet
现在,请考虑以下程序。
在这里,我们将函数的应用程序替换为它们的返回值,它确实改变了程序的语义。我们不再将问候语打印到屏幕上。这就是为什么打印被认为是副作用的原因,这就是为什么这个功能是不纯的。它不是参照透明的。greet
greet
现在,让我们考虑另一个例子。请考虑以下程序。
const main = async () => {
const response = await fetch("https://time.akamai.com/");
const serverTime = 1000 * await response.json();
const timeDiff = time => time - serverTime;
console.log("%d ms", timeDiff(Date.now()));
};
main();
显然,该功能是不纯的。但是,该函数是纯的还是不纯的?虽然它取决于哪个来自不纯的网络调用,但它仍然是引用透明的,因为它为相同的输入返回相同的输出,并且因为它没有任何副作用。main
timeDiff
serverTime
在这一点上,zerkms可能会不同意我的观点。在他的回答中,他说下面的例子中的函数是不纯的,因为“它传递地依赖于IO”。dollarToEuro
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
我必须不同意他的观点,因为来自数据库的事实是无关紧要的。这是一个内部细节,我们用于确定函数纯度的黑匣子方法并不关心内部细节。exchangeRate
在像Haskell这样的纯函数式语言中,我们有一个用于执行任意IO效果的逃生舱口。它被称为 unsafePerformIO
,顾名思义,如果你没有正确使用它,那么它就不安全,因为它可能会破坏引用透明度。但是,如果您确实知道自己在做什么,那么使用起来是完全安全的。
它通常用于从程序开头附近的配置文件加载数据。从配置文件加载数据是不纯的 IO 操作。但是,我们不希望将数据作为输入传递给每个函数而负担沉重。因此,如果我们使用,那么我们可以在顶层加载数据,并且我们所有的纯函数都可以依赖于不可变的全局配置数据。unsafePerformIO
请注意,仅仅因为函数依赖于从配置文件、数据库或网络调用加载的某些数据,并不意味着该函数是不纯的。
但是,让我们考虑一下具有不同语义的原始示例。
let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x) => {
return x * exchangeRate;
};
dollarToEuro(100) //90 today
dollarToEuro(100) //something else tomorrow
在这里,我假设因为它没有定义为,它将在程序运行时进行修改。如果是这样的话,那么绝对是一个不纯的函数,因为当修改时,它会破坏引用透明度。exchangeRate
const
dollarToEuro
exchangeRate
但是,如果变量未被修改并且将来永远不会被修改(即,如果它是常量值),那么即使它被定义为 ,它也不会破坏引用透明度。在这种情况下,确实是一个纯粹的函数。exchangeRate
let
dollarToEuro
请注意,每次再次运行程序时,的值都会更改,并且不会破坏引用透明度。仅当它在程序运行时发生变化时,它才会破坏引用透明度。exchangeRate
例如,如果您多次运行我的示例,那么您将获得不同的值,因此获得不同的结果。但是,由于 在程序运行时的 “从不”的值更改,因此该函数是纯函数。timeDiff
serverTime
serverTime
timeDiff