这是一个纯函数吗?

Sno*_*man 96 javascript functional-programming function

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

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

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

纯:

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

add(2, 4); // 6
Run Code Online (Sandbox Code Playgroud)

不纯:

let x = 2;

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

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)
Run Code Online (Sandbox Code Playgroud)

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

这部分我明白了。


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

(编辑- const在第一行中使用。let较早地使用。)

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
Run Code Online (Sandbox Code Playgroud)

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

Now, no matter how many times I call this function today, it will give me the same output for the input 100. However, it might give me a different output tomorrow. I'm not sure if this violates the first condition or not.

IOW, the function itself doesn't contain any logic to mutate the input, but it relies on an external constant that might change in the future. In this case, it's absolutely certain it will change daily. In other cases, it might happen; it might not.

Can we call such functions pure functions. If the answer is NO, how then can we refactor it to be one?

Cer*_*nce 112

The dollarToEuro's return value depends on an outside variable that is not an argument; therefore, the function is impure.

In the answer is NO, how then can we refactor the function to be pure?

One option is to pass in exchangeRate. This way, every time arguments are (something, somethingElse), the output is guaranteed to be something * somethingElse:

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

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};
Run Code Online (Sandbox Code Playgroud)

Note that for functional programming, you should avoid let - always use const to avoid reassignment.

  • 我认为您和@zerkms都是错误的。您似乎认为答案示例中的“ dollarToEuro”函数是不纯的,因为它取决于自由变量“ exchangeRate”。那太荒谬了。正如zerkms指出的那样,函数的纯度与它是否具有自由变量无关。但是,zerkms也是错误的,因为他认为“ dollarToEuro”函数是不纯的,因为它依赖于来自数据库的“ exchangeRate”。他说这是不纯的,因为“它暂时依赖于IO”。 (16认同)
  • @zerkms-我非常希望看到您对这个问题的回答(即使它只是改写使用某些不同术语的SomePerformance)。我认为这不会重复,而且会很有启发性,尤其是在引用时(理想情况下,其来源比上面的Wikipedia文章更好,但是,如果我们能做到的话,那还是个胜利)。(以某种否定的方式阅读此评论很容易。相信我,我是真诚的,我认为这样的回答将是不错的选择,并且希望阅读。) (8认同)
  • (续)再次,这很荒谬,因为它表明`dollarToEuro`是不纯的,因为`exchangeRate`是一个自由变量。这表明如果“ exchangeRate”不是自由变量,即如果它是一个自变量,则“ dollarToEuro”将是纯净的。因此,这表明`dollarToEuro(100)`是不纯净的,而`dollarToEuro(100,exchangeRate)`是纯净的。这显然是荒谬的,因为在两种情况下,您都依赖于来自数据库的`exchangeRate`。唯一的区别是,“ exchangeRate”是否是“ dollarToEuro”函数中的自由变量。 (8认同)
  • `const foo = 42; const add42 = x => x + foo;`<-这是另一个纯函数,它再次使用自由变量。 (6认同)
  • 没有自由变量**不是**函数必须是纯函数:`const add = x => y => x + y; const one = add(42);`这里的add和one是纯函数。 (4认同)
  • @zerkms不过,x是一个(咖喱的)参数,所以没关系(而不是返回值所依赖的非参数外部变量) (2认同)
  • @mbojko我不确定“外部变量”一词的含义。您能先定义一下吗?https://zh.wikipedia.org/wiki/Free_variables_and_bound_variables (2认同)

Aad*_*hah 69

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

相反,我们使用黑匣子考虑纯度。如果在给定相同输入的情况下某些代码总是产生相同的输出,则认为该代码是纯净的。根据此定义,即使内部使用了不正确的备忘录表,以下函数也是纯函数。

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));
Run Code Online (Sandbox Code Playgroud)

我们不在乎内部,因为我们使用黑匣子方法检查纯度。同样,我们不在乎所有代码最终都将转换为不纯的机器指令,因为我们正在考虑使用黑盒方法进行纯度分析。内部因素并不重要。

现在,考虑以下功能。

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");
Run Code Online (Sandbox Code Playgroud)

greet函数是纯函数还是纯函数?按照我们的黑盒方法,如果我们给它相同的输入(例如World),那么它总是将相同的输出打印到屏幕上(即Hello World!)。从这个意义上说,这不纯粹吗?不,这不对。它不纯净的原因是因为我们考虑在屏幕上打印一些东西。如果我们的黑匣子产生副作用,那么它不是纯净的。

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

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

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");
Run Code Online (Sandbox Code Playgroud)

在这里,我们内联了的定义,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();
Run Code Online (Sandbox Code Playgroud)

显然,main功能不纯。但是,该timeDiff函数是纯函数还是纯函数?尽管它取决于serverTime来自不正确的网络调用的消息,但它仍然是参照透明的,因为它为相同的输入返回相同的输出,并且没有任何副作用。

在这一点上,zerkms可能会不同意我的看法。他在回答中说,dollarToEuro以下示例中的功能不纯,因为“它暂时取决于IO”。

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

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};
Run Code Online (Sandbox Code Playgroud)

我必须不同意他的观点,因为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
Run Code Online (Sandbox Code Playgroud)

在这里,我假设因为exchangeRate未定义为const,所以它将在程序运行时进行修改。如果真是这样,那么dollarToEuro肯定是不纯函数,因为exchangeRate修改时,它将破坏参照透明性。

但是,如果该exchangeRate变量未修改并且以后将永远不会修改(即,如果它是一个常量值),那么即使将其定义为let,也不会破坏参照透明性。在那种情况下,dollarToEuro确实是一个纯函数。

请注意,exchangeRate每次重新运行程序时,的值都可以更改,并且不会破坏参照透明性。如果它在程序运行时发生更改,则只会破坏参照透明性。

例如,如果您timeDiff多次运行示例,则将获得不同的值serverTime,因此结果也将不同。但是,由于在serverTime程序运行时永不更改值,因此该timeDiff函数是纯函数。

  • 我不同意*”是不纯正的,因为它最终会编译成“将这个值移入eax”并将“将该值添加到eax的内容中”之类的指令。*如果通过装载或清除将“ eax”清除了,不管发生了什么,代码都保持确定性,因此是纯净的,否则,答案非常全面。 (5认同)
  • 这非常有用。谢谢。我的意思是在示例中使用了const。 (2认同)
  • 如果您确实打算使用const,那么dollarToEuro函数确实是纯函数。“ exchangeRate”的值更改的唯一方法是再次运行该程序。在这种情况下,旧过程和新过程是不同的。因此,它不会破坏参照透明性。这就像用不同的参数两次调用一个函数。参数可能不同,但是在函数内参数的值保持不变。 (2认同)
  • 这听起来有点像相对论:常数只是相对常数,而不是绝对常数,即相对于运行过程。显然,这里唯一正确的答案。+1。 (2认同)
  • 除了对函数的内部结构(“黑匣子”)务实之外,您还应该提到,在定义什么算作“相同结果”(以及什么是“结果”)的语义时,我们还需要务实。它可能不仅与返回值有关)。例如,我们不在乎函数返回的内容是否存储在堆栈的不同内存位置中-如您所说,“ *不是很有帮助*”。我们通常也不完全在乎对象身份。我们只关心某个相等性-通常只是隐含的,没有严格定义。 (2认同)
  • @Bergi:实际上,在具有不变值的纯语言中,身份无关紧要。只能通过以下方式来观察两个评估相同值的引用是对同一个对象还是对不同对象的两个引用:通过一个引用对对象进行“突变”,并观察通过另一个引用进行检索时该值是否也发生了变化。没有突变,身份就变得无关紧要。(正如Rich Hickey所说:身份是随着时间的流逝的一系列状态。) (2认同)
  • @JörgWMittag当然-但是javascript不是纯语言,即使具有`==`的不可变对象之间也可以观察到身份。 (2认同)

zer*_*kms 20

An answer of a me-purist (where "me" is literally me, since I think this question does not have a single formal "right" answer):

In a such dynamic language as JS with so many possibilities to monkey patch base types, or make up custom types using features like Object.prototype.valueOf it's impossible to tell whether a function is pure just by looking at it, since it's up to the caller on whether they want to produce side effects.

A demo:

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

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect
Run Code Online (Sandbox Code Playgroud)

An answer of me-pragmatist:

From the very definition from wikipedia

In computer programming, a pure function is a function that has the following properties:

  1. Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
  2. Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).

In other words, it only matters how a function behaves, not how it's implemented. And as long as a particular function holds these 2 properties - it's pure regardless how exactly it was implemented.

Now to your function:

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

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};
Run Code Online (Sandbox Code Playgroud)

It's impure because it does not qualify the requirement 2: it depends on the IO transitively.

I agree the statement above is wrong, see the other answer for details: /sf/answers/4112447461/

Other relevant resources:

  • 我不同意您的示例中的“ dollarToEuro”函数是不纯正的。我解释了为什么我不同意我的回答。/sf/answers/4112447461/ (5认同)
  • @TJCrowder`me`作为zerkms提供答案。 (4认同)
  • @bob ...或者这是一个阻止呼叫。 (4认同)
  • 是的,使用Javascript完全是信心,而不是保证 (2认同)

The*_*tor 13

就像其他答案所说的那样,您实施的方式dollarToEuro

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

const dollarToEuro = (x) => { return x * exchangeRate; }; 
Run Code Online (Sandbox Code Playgroud)

确实是纯净的,因为程序运行时不会更新汇率。但是,从概念上讲,这dollarToEuro似乎应该是一种不纯函数,因为它使用的是最新汇率。解释这种差异的最简单方法是您尚未实现,dollarToEuro而是实现了dollarToEuroAtInstantOfProgramStart

这里的关键是要计算货币换算需要几个参数,而通用的纯正版本dollarToEuro将提供所有这些参数。最直接的参数是要转换的美元数量以及汇率。但是,由于要从已发布的信息中获取汇率,因此现在需要提供三个参数:

  • 兑换金额
  • 咨询汇率的历史权威
  • 交易发生的日期(以索引历史权限)

这里的历史权限是您的数据库,并且假设该数据库没有受到损害,则在特定日期始终会返回相同的汇率结果。因此,结合使用这三个参数,您可以编写general的完全纯净,自给自足的版本,dollarToEuro看起来可能像这样:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());
Run Code Online (Sandbox Code Playgroud)

您的实现会在创建函数时立即捕获历史权限和交易日期的常量值-历史权限是您的数据库,捕获的日期是您启动程序的日期-剩下的就是美元金额,由调用者提供。不纯的版本dollarToEuro总是获取最新的值,本质上是隐式地使用date参数,将其设置为函数调用的瞬间,这不是纯粹的,因为您永远不能使用相同的参数调用函数两次。

如果您想要一个纯文本版本dollarToEuro仍可以获取最新值,则仍然可以绑定历史授权,但是不绑定date参数,并向调用方询问日期作为参数,最后像这样:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());
Run Code Online (Sandbox Code Playgroud)


Dav*_*lor 7

我想从JS的特定细节和形式化定义的抽象中退一步,并讨论为实现特定的优化需要保持哪些条件。通常,这是我们在编写代码时关心的主要内容(尽管它也有助于证明正确性)。函数式编程既不是最新时尚的指南,也不是自我否定的修道院宣言。它是解决问题的工具。

当您有这样的代码时:

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
Run Code Online (Sandbox Code Playgroud)

如果exchangeRate无法在的两次调用之间进行修改dollarToEuro(100),则可以记住第一次调用的结果dollarToEuro(100)并优化掉第二次调用。结果将是相同的,因此我们只能记住以前的值。

exchangeRate可能被设置一次,调用任何功能,看起来它之前,从来没有改变。限制性较小的是,您的代码可以exchangeRate一次查找特定功能或代码块,并在该范围内一致使用相同的汇率。或者,如果只有该线程可以修改数据库,则您有权假定,如果您没有更新汇率,则没有其他人可以更改它。

如果if fetchFromDatabase()本身是一个纯函数,求一个常量,并且exchangeRate是不可变的,我们可以在计算过程中将其始终折叠。知道是这种情况的编译器可以做出与注释中相同的推论,得出的结果dollarToEuro(100)为90.0,并将整个表达式替换为常量90.0。

但是,如果fetchFromDatabase()不执行被认为是副作用的I / O,则其名称违反了“最小惊讶原则”。


War*_*rbo 6

为了扩展其他人关于引用透明性的观点:我们可以将纯度定义为简单的函数调用的引用透明性(即,对函数的每个调用都可以由返回值替换,而无需更改程序的语义)。

你给这两个属性是两个后果引用透明的。例如,以下函数f1是不纯的,因为它每次都不会给出相同的结果(编号为1的属性):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}
Run Code Online (Sandbox Code Playgroud)

为什么每次获得相同的结果很重要?因为获得不同的结果是函数调用与值具有不同语义的一种方法,因此破坏了引用透明性。

假设我们编写代码f1("hello", "world"),然后运行它并获得返回值"hello"。如果我们查找/替换每个调用f1("hello", "world")并将其替换为,"hello"我们将更改程序的语义(所有调用现在都将替换为"hello",但最初大约有一半会评估为"world")。因此,对to f1的调用不是参照透明的,因此f1是不纯的。

函数调用可以具有与值不同的语义的另一种方式是通过执行语句。例如:

function f2(x) {
  console.log("foo");
  return x;
}
Run Code Online (Sandbox Code Playgroud)

的返回值f2("bar")将始终为"bar",但是该值的语义"bar"与调用不同,f2("bar")因为后者也将记录到控制台。用另一个替换一个会更改程序的语义,因此它不是参照透明的,因此f2是不纯的。

您的dollarToEuro函数是否是参照透明的(因此是纯净的)取决于两件事:

  • 我们认为参照透明的“范围”
  • exchangeRate意愿是否会在该“范围”内改变

没有“最佳”使用范围。通常,我们会考虑程序的一次运行或项目的生命周期。以此类推,假设每个函数的返回值都被缓存了(例如@ aadit-m-shah给出的示例中的备忘录表):我们何时需要清除缓存,以确保陈旧的值不会干扰我们的语义?

如果exchangeRate使用的var话,它可能在每次调用时都改变dollarToEuro; 我们将需要清除每次调用之间的所有缓存结果,因此就没有参照透明性了。

通过使用const我们将“作用域”扩展到程序的运行:将返回值缓存dollarToEuro到程序完成之前是安全的。我们可以想象使用宏(使用Lisp这样的语言)将函数调用替换为其返回值。对于配置值,命令行选项或唯一ID之类的东西,这种纯度是很常见的。如果我们只限于思考程序的一个运行,那么我们得到的最纯洁的好处,但我们必须要小心跨过运行(例如,将数据保存到一个文件,然后在另一个运行加载它)。我不会从抽象的意义上将这些函数称为“ pure” (例如,如果我正在编写字典定义),但是在上下文中将它们视为纯函数没有问题。

如果我们将项目的生命周期视为我们的“范围”,那么即使从抽象的意义上来说,我们也是“最参照透明”的,因此也是“最纯净的”。我们将永远不需要清除假设的缓存。我们甚至可以通过直接重写磁盘上的源代码来执行“缓存”,以将调用替换为其返回值。这甚至可以项目工作,例如,我们可以想象一个在线的函数及其返回值数据库,其中任何人都可以查找函数调用,并且(如果它在数据库中)可以使用由另一端某人提供的返回值。多年前在另一个项目上使用相同功能的世界。


Jes*_*ard 6

此函数不是纯函数,它依赖于外部变量,几乎肯定会更改该变量。

因此,该函数使您所做的第一点失败,对于相同的参数,它不会返回相同的值。

要使此函数“纯”,请exchangeRate作为参数传入。

然后,这将满足两个条件。

  1. 当传递相同的值和汇率时,它将始终返回相同的值。
  2. 它也没有副作用。

示例代码:

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

dollarToEuro(100, fetchFromDatabase())
Run Code Online (Sandbox Code Playgroud)

  • “这几乎肯定会改变”——不是,而是“const”。 (2认同)