Sno*_*man 96 javascript functional-programming function
大多数源将纯函数定义为具有以下两个属性:
这是与我有关的第一个条件。在大多数情况下,很容易判断。考虑以下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.
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它没有改变程序的语义。
现在,考虑以下程序。
undefined;
undefined;Run Code Online (Sandbox Code Playgroud)
在这里,我们将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函数是纯函数。
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:
- 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).
- 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:
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)
我想从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,则其名称违反了“最小惊讶原则”。
为了扩展其他人关于引用透明性的观点:我们可以将纯度定义为简单的函数调用的引用透明性(即,对函数的每个调用都可以由返回值替换,而无需更改程序的语义)。
你给这两个属性是两个后果引用透明的。例如,以下函数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” (例如,如果我正在编写字典定义),但是在上下文中将它们视为纯函数没有问题。
如果我们将项目的生命周期视为我们的“范围”,那么即使从抽象的意义上来说,我们也是“最参照透明”的,因此也是“最纯净的”。我们将永远不需要清除假设的缓存。我们甚至可以通过直接重写磁盘上的源代码来执行“缓存”,以将调用替换为其返回值。这甚至可以跨项目工作,例如,我们可以想象一个在线的函数及其返回值数据库,其中任何人都可以查找函数调用,并且(如果它在数据库中)可以使用由另一端某人提供的返回值。多年前在另一个项目上使用相同功能的世界。
此函数不是纯函数,它依赖于外部变量,几乎肯定会更改该变量。
因此,该函数使您所做的第一点失败,对于相同的参数,它不会返回相同的值。
要使此函数“纯”,请exchangeRate作为参数传入。
然后,这将满足两个条件。
示例代码:
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
dollarToEuro(100, fetchFromDatabase())
Run Code Online (Sandbox Code Playgroud)