这是一个纯函数吗?

大多数将纯函数定义为具有以下两个属性:

  1. 对于相同的参数,其返回值是相同的。
  2. 它的评估没有副作用。

这是我关心的第一个条件。在大多数情况下,很容易判断。请考虑以下 JavaScript 函数(如本文所示))

纯:

const add = (x, y) => x + y;

add(2, 4); // 6

㛾:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

很容易看出,第二个函数将为后续调用提供不同的输出,从而违反了第一个条件。因此,它是不纯洁的。

这部分我明白了。


现在,对于我的问题,请考虑以下函数,它将以美元为单位的给定金额转换为欧元:

(编辑 - 在第一行中使用。早先无意中使用过。constlet

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

假设我们从数据库获取汇率,它每天都在变化。

现在,无论我今天调用此函数多少次,它都会为我提供相同的输入输出 。但是,它可能会在明天给我一个不同的输出。我不确定这是否违反了第一个条件。100

IOW,函数本身不包含任何改变输入的逻辑,但它依赖于将来可能会更改的外部常量。在这种情况下,绝对可以肯定的是它会每天变化。在其他情况下,它可能会发生;它可能不会。

我们可以称这些函数为纯函数吗?如果答案是否定的,那么我们怎么能把它重构成一个呢?


答案 1

的返回值取决于一个不是参数的外部变量;因此,该函数是不纯的。dollarToEuro

如果答案是否定的,那么我们如何才能将函数重构为纯函数呢?

一种选择是传入 。这样,每次参数为 ,输出保证为:exchangeRate(something, somethingElse)something * somethingElse

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

请注意,对于函数式编程,应避免 - 始终使用以避免重新分配。letconst


答案 2

从技术上讲,您在计算机上执行的任何程序都是不纯的,因为它最终会编译为诸如“将此值移入”和“将此值添加到”之类的指令,这些指令是不纯的。这不是很有帮助。eaxeax

相反,我们使用黑匣子来考虑纯度。如果某些代码在给定相同的输入时总是产生相同的输出,那么它被认为是纯的。根据此定义,即使内部它使用不纯的 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");

函数是纯的还是不纯的?根据我们的黑匣子方法,如果我们给它相同的输入(例如),那么它总是将相同的输出打印到屏幕上(即)。从这个意义上说,它不是纯粹的吗?不,它不是。它不纯净的原因是,我们认为将某些东西打印到屏幕上是一种副作用。如果我们的黑匣子产生副作用,那么它就不是纯粹的。greetWorldHello World!

什么是副作用?这就是参照透明度概念有用的地方。如果一个函数在引用上是透明的,那么我们总是可以用它们的结果替换该函数的应用程序。请注意,这与函数内联不同。

在函数内联中,我们将函数的应用程序替换为函数的主体,而不改变程序的语义。但是,引用透明函数始终可以替换为其返回值,而不会更改程序的语义。请考虑以下示例。

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

在这里,我们内联了的定义,并且它没有改变程序的语义。greet

现在,请考虑以下程序。

undefined;
undefined;

在这里,我们将函数的应用程序替换为它们的返回值,它确实改变了程序的语义。我们不再将问候语打印到屏幕上。这就是为什么打印被认为是副作用的原因,这就是为什么这个功能是不纯的。它不是参照透明的。greetgreet

现在,让我们考虑另一个例子。请考虑以下程序。

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();

显然,该功能是不纯的。但是,该函数是纯的还是不纯的?虽然它取决于哪个来自不纯的网络调用,但它仍然是引用透明的,因为它为相同的输入返回相同的输出,并且因为它没有任何副作用。maintimeDiffserverTime

在这一点上,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

在这里,我假设因为它没有定义为,它将在程序运行时进行修改。如果是这样的话,那么绝对是一个不纯的函数,因为当修改时,它会破坏引用透明度。exchangeRateconstdollarToEuroexchangeRate

但是,如果变量未被修改并且将来永远不会被修改(即,如果它是常量值),那么即使它被定义为 ,它也不会破坏引用透明度。在这种情况下,确实是一个纯粹的函数。exchangeRateletdollarToEuro

请注意,每次再次运行程序时,的值都会更改,并且不会破坏引用透明度。仅当它在程序运行时发生变化时,它才会破坏引用透明度。exchangeRate

例如,如果您多次运行我的示例,那么您将获得不同的值,因此获得不同的结果。但是,由于 在程序运行时的 “从不”的值更改,因此该函数是纯函数。timeDiffserverTimeserverTimetimeDiff