大多数sources将纯函数定义为具有以下两个属性:
这是与我有关的第一个条件。在大多数情况下,很容易判断。考虑以下JavaScript函数(如this article所示)
纯:
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)
很容易看出第二个函数将为后续调用提供不同的输出,从而违反了第一个条件。因此,这是不纯的。
这部分我明白了。
现在,对于我的问题,考虑以下函数,该函数会将给定的美元金额转换为欧元:
(编辑-在第一行中使用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
假设我们从数据库获取汇率,并且汇率每天都在变化。
现在,无论我今天 多少次调用此函数,它都会为输入100
提供相同的输出。但是,明天可能会给我不同的输出。我不确定这是否违反第一个条件。
IOW,该函数本身不包含任何使输入突变的逻辑,但是它依赖于将来可能更改的外部常量。在这种情况下,绝对可以每天更改。在其他情况下,可能会发生;可能不会。
我们可以将此类函数称为纯函数吗?如果答案是否定的,那么我们如何将其重构为一个?
答案 0 :(得分:127)
dollarToEuro
的返回值取决于不是参数的外部变量;因此,该功能不纯。
答案是否定的,那么我们如何才能将函数重构为纯函数?
一种选择是传递exchangeRate
。这样,每次参数为(something, somethingElse)
时,输出保证为something * somethingElse
:
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
请注意,对于函数式编程,应避免使用let
-始终使用const
以避免重新分配。
答案 1 :(得分:74)
从技术上讲,您在计算机上执行的任何程序都是不纯正的,因为它最终会编译为诸如“将该值移至eax
并将“将该值添加到eax
的内容”之类的指令,不纯净的那不是很有帮助。
相反,我们考虑使用black boxes的纯度。如果某些代码在给定相同的输入时总是产生相同的输出,则认为它是纯净的。根据此定义,即使内部使用了不正确的备忘录表,以下函数也是纯函数。
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!
)。从这个意义上说,这不纯粹吗?不,这不对。它不纯净的原因是因为我们考虑在屏幕上打印一些东西。如果我们的黑匣子产生了副作用,那就不是纯粹的。
副作用是什么?这是referential transparency概念有用的地方。如果一个函数是参照透明的,那么我们总是可以用其结果替换该函数的应用程序。请注意,这与function inlining不同。
在函数内联中,我们用函数的主体替换了函数的应用程序,而没有改变程序的语义。但是,始终可以将引用透明函数替换为其返回值,而无需更改程序的语义。请考虑以下示例。
console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");
在这里,我们插入了greet
的定义,但它没有改变程序的语义。
现在,考虑以下程序。
undefined;
undefined;
在这里,我们用返回值替换了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在这一点上可能会与我不同意。在他的answer中,他说以下示例中的dollarToEuro
函数是不纯的,因为“它暂时依赖于IO。”
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
函数是纯函数。
答案 2 :(得分:21)
一个纯粹主义者的答案(其中“我”实际上是我,因为我认为这个问题没有一个单独的 formal “正确”答案):
在像JS这样的动态语言中,有很多可能性可以修补基本类型,或者使用Object.prototype.valueOf
之类的功能来构成自定义类型,仅通过查看就无法判断一个函数是否纯净,因为由呼叫者决定是否要产生副作用。
演示:
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
我是实用主义者的答案:
从very definition from wikipedia
在计算机编程中,纯函数是具有以下属性的函数:
- 对于相同的参数,其返回值是相同的(局部静态变量,非局部变量,可变引用参数或来自I / O设备的输入流无变化)。
- 它的评估没有副作用(局部静态变量,非局部变量,可变引用参数或I / O流没有突变)。
换句话说,仅关系到函数的行为方式,而不是函数的实现方式。只要一个特定的函数拥有这2个属性-不管实现的方式如何,都是纯函数。
现在可以使用您的功能
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
这是不纯净的,因为它不符合要求2:可传递地依赖IO。
我同意以上说法是错误的,有关详细信息,请参见其他答案:https://stackoverflow.com/a/58749249/251311
其他相关资源:
答案 3 :(得分:14)
就像其他答案一样,您实现dollarToEuro
的方式,
let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x) => { return x * exchangeRate; };
确实是纯正的,因为程序运行时汇率不会更新。但是,从概念上讲,dollarToEuro
似乎应该是一个不纯函数,因为它使用的是最新汇率。解释这种差异的最简单方法是您没有实现dollarToEuro
,而是实现了dollarToEuroAtInstantOfProgramStart
。
关键是要计算货币换算需要几个参数,而通用dollarToEuro
的纯正版本将提供所有这些参数。最直接的参数是要转换的美元数量以及汇率。但是,由于要从已发布的信息中获取汇率,因此现在需要提供三个参数:
这里的历史权限是您的数据库,并且假设该数据库没有受到损害,将始终在特定日期返回相同的汇率结果。因此,结合使用这三个参数,您可以编写通用dollarToEuro
的完全纯净,自给自足的版本,看起来可能像这样:
function dollarToEuro(x, authority, date) {
const exchangeRate = authority(date);
return x * exchangeRate;
}
dollarToEuro(100, fetchFromDatabase, Date.now());
您的实现会在创建函数时立即捕获历史权限和交易日期的常量值-历史权限是您的数据库,捕获的日期是您启动程序的日期-剩下的就是呼叫者提供的美元金额。总是获取最新值的dollarToEuro
的不纯版本实际上隐含了date参数,将其设置为函数调用的瞬间,这不是纯粹的,因为您永远无法使用两次相同的参数。
如果您想要的是dollarToEuro
的纯版本仍可以获取最新值,则仍然可以绑定历史授权,但不绑定date参数并要求输入日期调用方作为参数,最后是这样的:
function dollarToEuro(x, date) {
const exchangeRate = fetchFromDatabase(date);
return x * exchangeRate;
}
dollarToEuro(100, Date.now());
答案 4 :(得分:8)
我想从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
如果在两次调用exchangeRate
之间从未修改过dollarToEuro(100)
,则可以记住第一次调用dollarToEuro(100)
的结果并优化第二次调用。结果将是相同的,因此我们只需记住以前的值即可。
在调用任何查找exchangeRate
的函数之前,它只能被设置一次,并且从不修改。限制性较小的是,您的代码可能会针对特定功能或代码块一次查询exchangeRate
,并在该范围内一致地使用相同的汇率。或者,如果只有该线程可以修改数据库,则您有权假定,如果您不更新汇率,则没有其他人对您进行过更改。
如果fetchFromDatabase()
本身是一个对常数求值的纯函数,并且exchangeRate
是不可变的,则我们可以在计算过程中将其常数折叠。知道是这种情况的编译器可以做出与注释中相同的推论,即dollarToEuro(100)
的计算结果为90.0,并用常量90.0替换整个表达式。
但是,如果fetchFromDatabase()
不执行I / O(被认为是副作用),则其名称违反了“最小惊讶原则”。
答案 5 :(得分:8)
此函数不是纯函数,它依赖于外部变量,几乎肯定会发生变化。
因此,该函数将使您所做的第一点失败,对于相同的参数,它不会返回相同的值。
要使此函数“纯净”,请传入exchangeRate
作为参数。
这将同时满足两个条件。
示例代码:
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
dollarToEuro(100, fetchFromDatabase())
答案 6 :(得分:7)
为了扩展其他人在引用透明性方面的观点:我们可以将纯度定义为简单的函数调用的引用透明性(即,对函数的每个调用都可以由返回值替换,而无需更改程序的语义)。
您提供的两个属性都是参照透明性的后果。例如,以下函数f1
是不正确的,因为它每次都不会给出相同的结果(您为属性编号1):
function f1(x, y) {
if (Math.random() > 0.5) { return x; }
return y;
}
为什么每次都获得相同的结果很重要?因为获得不同的结果是函数调用具有与值不同的语义的一种方式,因此破坏了引用透明性。
假设我们编写了代码f1("hello", "world")
,然后运行它并获得返回值"hello"
。如果我们查找/替换每个调用f1("hello", "world")
并将其替换为"hello"
,我们将更改程序的语义(所有调用现在都将替换为"hello"
,但是最初,其中大约一半会评估为"world"
)。因此,对f1
的调用不是参照透明的,因此f1
是不纯的。
函数调用可以具有与值不同的语义的另一种方法是执行语句。例如:
function f2(x) {
console.log("foo");
return x;
}
f2("bar")
的返回值将始终为"bar"
,但是值"bar"
的语义与调用f2("bar")
不同,因为后者也将登录到安慰。用另一个替换一个会改变程序的语义,因此它不是参照透明的,因此f2
是不纯的。
您的dollarToEuro
函数是否是参照透明的(因此是纯函数)取决于两件事:
exchangeRate
是否会在该“范围”内发生变化没有“最佳”使用范围;通常,我们会考虑程序的一次运行或项目的生命周期。以此类推,假设每个函数的返回值都被缓存了(例如@ aadit-m-shah给出的示例中的备忘录表):我们何时需要清除缓存,以确保陈旧的值不会干扰我们的语义?
如果exchangeRate
使用的是var
,则在每次调用dollarToEuro
之间可能会改变;我们将需要清除每个调用之间的所有缓存结果,因此没有参照透明性。
通过使用const
,我们将“作用域”扩展到程序的运行:可以安全地缓存dollarToEuro
的返回值,直到程序完成。我们可以想象使用宏(使用Lisp这样的语言)将函数调用替换为其返回值。对于配置值,命令行选项或唯一ID之类的东西,这种纯度是很常见的。如果我们只考虑运行一次程序,那么我们将获得纯净的大部分好处,但是我们必须小心地跨过 运行(例如,将数据保存到文件中,然后将其加载到另一个文件中)跑)。我不会从抽象的意义上将这些函数称为“ pure”(例如,如果我正在编写字典定义),但是在上下文中将它们视为纯没有问题
如果我们将项目的生命周期视为我们的“范围”,那么即使从抽象的意义上来说,我们也是“最透明的”,因此也是“最纯净的”。我们将永远不需要清除假设的缓存。我们甚至可以通过直接重写磁盘上的源代码来执行“缓存”,以将调用替换为其返回值。这甚至可以在整个项目中正常运行,例如我们可以想象一个在线的函数及其返回值数据库,在那里任何人都可以查找函数调用,并且(如果它在数据库中)可以使用世界另一端使用同一函数的人提供的返回值在另一个项目上。
答案 7 :(得分:4)
如所写,它是一个纯函数。它不会产生副作用。该函数具有一个形式参数,但具有两个输入,并且对于任何两个输入将始终输出相同的值。
答案 8 :(得分:2)
我们可以将此类函数称为纯函数吗?如果答案是否定的,那么我们如何将其重构为一个?
正如您适当指出的,“明天可能会给我带来不同的输出” 。如果是这样,答案将是响亮的“否” 。如果您对dollarToEuro
的预期行为已正确解释为:
const dollarToEuro = (x) => {
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
return x * exchangeRate;
};
但是,存在另一种解释,在这种解释中,纯解释是这样的:
const dollarToEuro = ( () => {
const exchangeRate = fetchFromDatabase();
return ( x ) => x * exchangeRate;
} )();
上面的 dollarToEuro
是纯净的。
从软件工程的角度来看,必须声明dollarToEuro
对函数fetchFromDatabase
的依赖性。因此,重构dollarToEuro
的定义如下:
const dollarToEuro = ( x, fetchFromDatabase ) => {
return x * fetchFromDatabase();
};
有了这个结果,在fetchFromDatabase
令人满意地运行的前提下,我们可以得出结论,fetchFromDatabase
在dollarToEuro
上的投影必须令人满意。或语句“ fetchFromDatabase
是纯净的”表示dollarToEuro
是纯净的(因为fetchFromDatabase
是dollarToEuro
的 基础 通过x
的标量因子。
从原始帖子中,我可以了解到fetchFromDatabase
是一个函数时间。让我们改进重构工作以使这种理解变得透明,从而明确将fetchFromDatabase
限定为纯函数:
fetchFromDatabase =(时间戳)=> {/ *这里是实现* /};
最终,我将重构功能如下:
const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };
// Do a partial application of `fetchFromDatabase`
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );
const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();
因此,dollarToEuro
可以通过简单地证明其正确调用fetchFromDatabase
(或其派生的exchangeRate
)来进行单元测试。
答案 9 :(得分:0)
我对将此类功能归类为纯功能有什么疑问,好像我开始将其与其他“纯功能”一起使用时一样。
我想我更喜欢“纯”的含义,我可以毫无意外地构成它。
这就是我认为的“功能核心”:
// builder of Rates Expressions, only depends on ```map```
const ratesExpr = (f) => (rates => rates.map(f))
// The actual pure function
const dollarToEuro = (x) => ratesExpr( r => r.usd.eur * x)
// base interpreter of Rates Expressions
const evalRatesExpr = fetcher => expr => expr([fetcher()])
命令式外壳:
// various interpreters with live/cached data
const testRatesExpr = evalRatesExpr( () => { usd = { eur = 2.0 }} )
const cachedRates = fetchFromDatabase()
const evalCachedRatesExpr = evalRatesExpr(() => cachedRates)
const evalLiveRatesExpr = evalRatesExpr( fetchFromDatabase )
// Some of these may pass...
assert (testRatesExpr(dollarToEuro(5))) === [10] //Every time
assert (evalLiveRatesExpr(dollarToEuro(5)) === [8] //Rarely
assert (evalCacheRatesExpr(dollarToEuro(5)) === [8.5] //Sometimes
没有类型,很难使整个东西粘在一起。我认为这是“最终无标签”和“单子”组合。
答案 10 :(得分:-3)
我是Haskell / JS的双语者,而Haskell是关于函数纯度的很多语言之一,所以我想我将从Haskell的角度为您提供一个视角。
就像其他人所说的那样,在Haskell中,读取 mutable 变量通常被认为是不纯的。 变量和定义之间的区别在于变量可以稍后更改,定义永远相同。因此,如果您已经声明了const
(假设它只是一个number
并且没有可变的内部结构),那么从中读取将使用纯净的定义。但是您想对随时间变化的汇率建模,这需要某种可变性,然后您就会陷入困境。
在Haskell中,为了描述这类不纯的事物(我们可以称其为“效应”,以及它们的使用是“有效的”而不是“纯粹的”),我们进行了您可能称为元编程的事情。今天的元编程通常指的是<宏>宏,这不是我的意思,而仅仅是编写一个程序以编写另一个程序的想法。
在这种情况下,我们在Haskell中编写了一个纯计算,它计算出一个有效的程序,然后该程序将执行我们想要的操作。因此,Haskell源文件(至少是一个描述程序而不是库的文件)的全部目的是描述一个有效的程序的纯计算,该程序会产生无效的main
。然后,Haskell编译器的工作是获取此源文件,执行纯计算,然后将有效的程序作为二进制可执行文件放在硬盘驱动器上的某个位置,以便以后有空运行。换句话说,在纯计算运行(编译器生成可执行文件的时间)与有效程序运行(无论运行可执行文件的时间)之间存在时间间隔。
所以对我们来说,有效的程序实际上是数据结构,并且它们只是被提及而没有内在地做任何事情(除了返回值之外,它们没有*副作用);它们的返回值包含其影响)。对于一个非常轻量的TypeScript类示例,它描述了不可变程序以及您可以使用它们做的一些事情,
export class Program<x> {
// wrapped function value
constructor(public run: () => Promise<x>) {}
// promotion of any value into a program which makes that value
static of<v>(value: v): Program<v> {
return new Program(() => Promise.resolve(value));
}
// applying any pure function to a program which makes its input
map<y>(fn: (x: x) => y): Program<y> {
return new Program(() => this.run().then(fn));
}
// sequencing two programs together
chain<y>(after: (x: x) => Program<y>): Program<y> {
return new Program(() => this.run().then(x => after(x).run()));
}
}
关键是,如果您有Program<x>
,则不会发生任何副作用,而这些都是完全功能纯净的实体。除非程序不是纯函数,否则在程序上映射函数不会有任何副作用。对两个程序进行排序不会产生任何副作用;等
例如,在您的情况下如何应用此代码,您可能会编写一些纯函数,这些函数将返回程序以按ID获取用户并更改数据库并获取JSON数据,例如
// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
return new Program(() => fetch(url).then(response => response.json()));
}
然后您可以描述一个cron作业来卷曲URL并查找一些员工,并以一种纯粹的功能性方式通知其主管,
const action =
fetchJSON('http://myapi.example.com/employee-of-the-month')
.chain(eotmInfo => getUserById(eotmInfo.id))
.chain(employee =>
getUserById(employee.supervisor_id)
.chain(supervisor => notifyUserById(
supervisor.id,
'Your subordinate ' + employee.name + ' is employee of the month!'
))
);
关键是这里的每个函数都是完全纯函数;在我实际上action.run()
使其开始运动之前,实际上没有发生任何事情。另外,我可以编写类似的功能
// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
return new Program(() => Promise.all([x.run(), y.run()]));
}
如果JS承诺取消,我们可以让两个程序相互竞争并获得第一个结果,然后取消第二个结果。 (我是说我们仍然可以,但是不清楚该怎么做。)
类似地,在您的情况下,我们可以描述汇率的变化
declare const exchangeRate: Program<number>;
function dollarsToEuros(dollars: number): Program<number> {
return exchangeRate.map(rate => dollars * rate);
}
和exchangeRate
可能是一个看似可变值的程序,
let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
return Promise.resolve(privateExchangeRate);
});
但即使这样,此函数dollarsToEuros
现在是从数字到生成数字的程序的纯函数,您可以以确定性的方程方式对其进行推理,从而可以对具有以下内容的任何程序进行推理没有副作用。
当然,这样做的代价是您必须最终将.run()
称为某个地方,这将是不纯的。但是,您可以通过纯计算来描述整个计算结构,并且可以将杂质推到代码的边缘。