这是一个纯函数吗?

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

  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)

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

这部分我懂。


现在,对于我的问题,考虑这个函数,它将给定的美元转换成欧元:

(编辑-在第一行中使用 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,函数本身不包含任何逻辑来改变输入,但它依赖于一个可能在将来改变的外部常量。在这种情况下,它肯定会每天变化。在其他情况下,这种情况可能会发生,也可能不会。

我们能否调用这样的函数纯函数。如果答案是否定的,那么我们如何将其重构为一个?

12610 次浏览

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以避免重新分配。

一个纯粹主义者的答案(“我”字面上就是我,因为我认为这个问题没有一个单一的 正式的“正确”答案) :

在像 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

我的答案——实用主义者:

来自 非常符合维基百科的定义

在计算机编程中,纯函数是具有以下属性的函数:

  1. 对于相同的参数,它的返回值是相同的(局部静态变量、非局部变量、可变的引用参数或来自 I/O 设备的输入流没有变化)。
  2. 它的计算没有副作用(没有局部静态变量、非局部变量、可变引用参数或 I/O 流的突变)。

换句话说,它只关心函数的行为,而不关心它是如何实现的。只要一个特定的函数拥有这两个属性,不管它是如何实现的,它都是纯粹的。

现在说说你的职责:

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

其他相关资源:

从技术上讲,你在计算机上执行的任何程序都是不纯的,因为它最终会被编译成这样的指令: 将这个值移入 eax & rdquo; 并将这个值加入到 eax & rdquo; 的内容中,这些内容都是不纯的。这可没什么帮助。

相反,我们使用 黑匣子来考虑纯度。如果一些代码在给定相同的输入时总是产生相同的输出,那么它就被认为是纯的。根据这个定义,下面的函数也是纯的,即使它在内部使用了一个不纯的备忘录表。

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的定义,它没有改变程序的语义。

现在,考虑下面的程序。

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 可能不同意我的观点。在他的 回答中,他说下面例子中的 dollarToEuro函数是不纯粹的,因为它传递性地依赖于 IO。& rdquo;

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函数是纯的。

我想从 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

如果在对 dollarToEuro(100)的两次调用之间不能修改 exchangeRate,就有可能记录对 dollarToEuro(100)的第一次调用的结果并优化掉第二次调用。结果是一样的,所以我们只需要记住之前的值。

在调用任何查找 exchangeRate的函数之前,可以设置 exchangeRate一次,并且永远不要修改它。限制较少的情况下,您可以让代码查找一次特定函数或代码块的 exchangeRate,并在该范围内始终使用相同的兑换率。或者,如果只有这个线程可以修改数据库,您将有权假定,如果您没有更新汇率,没有其他人对您进行了更改。

如果 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;
}

为什么每次都得到相同的结果很重要?因为得到不同的结果是函数调用与值具有不同语义的一种方式,从而打破了参照透明度(计算机科学)。

假设我们编写代码 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 之类的东西来说是常见的。如果我们限制自己只考虑程序的一次运行,那么我们就会得到纯粹性的大部分好处,但是我们必须小心地运行 穿过去(例如,将数据保存到一个文件中,然后在另一次运行中加载它)。在 摘要的意义上,我不会将这些函数称为“纯”(例如,如果我正在编写字典定义) ,但是将它们视为纯 在上下文中没有问题。

如果我们把项目的生命周期看作是我们的“范围”,那么我们就是“最具参照透明度”,因此也是“最纯粹的”,即使在抽象意义上也是如此。我们永远不需要清除假设的缓存。我们甚至可以通过直接重写磁盘上的源代码来实现这种“缓存”,用返回值替换调用。这甚至可以用于 穿过去项目,例如我们可以想象一个在线的函数及其返回值数据库,在这里任何人都可以查找一个函数调用,并且(如果它在数据库中)使用世界另一端的某个人提供的返回值,这个人几年前在不同的项目中使用了相同的函数。

正如其他答案所说,你实现 dollarToEuro的方式,

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


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

确实是纯粹的,因为在程序运行时汇率没有更新。然而,从概念上看,dollarToEuro似乎应该是一个不纯粹的函数,因为它使用最新的汇率。解释这种差异的最简单的方法是,你没有实现 dollarToEuro,而是实现了 dollarToEuroAtInstantOfProgramStart——这里的关键是,计算货币兑换需要几个参数,一个真正纯粹的通用 dollarToEuro版本将提供所有这些参数。

正如其他答案所显示的那样,你可以提供的最直接的参数是要转换的美元数额,以及每美元兑换多少欧元的汇率:

const dollarToEuro = (x, exchangeRate) => x * exchangeRate;

然而,这样一个函数是非常没有意义的—— dollarToEuro的调用者会精确地调用它,因为他们不知道汇率,并期望 dollarToEuro知道汇率并将其应用到他们想要的货币兑换中。

然而,我们还知道: 在任何给定的时刻,汇率总是相同的,如果你有一个来源(可能是一个数据库) ,在汇率变化时公布它们,那么我们可以按日期查找这个来源,并计算出在任何特定的一天汇率将会是什么。在代码中,这意味着为 fetchFromDatabase()函数提供一个日期参数:

function fetchFromDatabase(date) {
// make the REST call to the database, providing the date as a parameter ...
// once it's done, return the result
}

如果数据库在输入相同的日期时总是返回相同的汇率结果,那么 fetchFromDatabase()是纯的。有了这样一个函数,你就可以得到这样一个函数:

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

也会是纯洁的。

现在,回到你最初的功能。如果我们将它重写到这个新的 dollarToEuro(x, date)的新框架中,它会是这样的:

const programStartDate = Date.now();


const dollarToEuroAtInstantOfProgramStart = (x) => {
return dollarToEuro(x, programStartDate);
}

如果我们想编写一个函数,使用数据库中最新的值来转换货币,我们可以这样写:

const dollarToEuroUpToDate = (x) => { return dollarToEuro(x, Date.now()); }

这个函数不是纯函数,因为(而且仅仅是因为) Date.now()不是纯函数——这正是我们所期望的。

这个函数不是纯函数,它依赖于一个外部变量,这个变量几乎肯定会发生变化。

因此,函数在第一个点失败,对于相同的参数,它不返回相同的值。

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

这将满足这两个条件。

  1. 当传入相同的值和汇率时,它总是返回相同的值。
  2. 也不会有副作用。

示例代码:

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


dollarToEuro(100, fetchFromDatabase())

如前所述,它是一个纯函数。没有副作用。函数有一个形式参数,但它有两个输入,并且对于任意两个输入总是输出相同的值。

正如最上面的答案所说,阅读 a 易变的值通常被认为是不纯的,你可以重构以包括汇率。没有说明的是,某个时候以后,你将需要一个不纯粹的功能来做真正的工作在不纯粹的世界,类似的东西

async function buyCoins(user: User, package: CoinPackage) {
// Random number generation is impure
const id = uuidv4();
// Fetching from a DB is impure,
const exchageRates = await knex.select('*').from('ExchangeRates');
// usdFromPrice can be a pure function
const usdEstimate = usdFromPrice(package.price, exchangeRates);
// but getting a date is not
const createdDate = Date.now() / 1000;
// Saving to a DB is more obviously impure
const coinTransfer = { id, user, package, state: "PENDING", usdEstimate, createdDate };
await knex('CoinTransfers').insert(coinTransfer);
// ...
}

通过一个非常简单的标准可以看出,从可变值、日期或随机数中读取数据是不纯粹的。为什么我们喜欢纯洁?因为纯函数可以组合、缓存、优化、内联,最重要的是 隔离测试。这就是为什么模拟如此流行,模拟将有效的计算转化为纯计算,允许单元测试等等。

测试是询问是否有纯函数的一种很好的方式。在您的情况下,我可能会编写一个测试,“确认10 something 是11.93 US $”,这个测试明天就会中断!所以我不得不嘲笑它的副作用,这就证明了确实有副作用。日期是副作用,sleep () ing 是副作用,这些东西在 lambda 微积分的抽象世界中没有真正的表达能力ーー你可以从你可能想要模拟时间的事实中看出这一点,例如测试一些东西,比如“你可以在发送后编辑一条推文15分钟,但在那之后编辑应该被冻结。”

纯默认情况下是什么样子的?

在像 Haskell 这样的语言中,我们力求使用 在默认情况下是纯洁的,你仍然可以在顶部编写 const rate = getExchangeRate()代码行,但是它需要一个名为 unsafePerformIO的函数,它的名称中就有“不安全”这个词。例如,我可能正在写一个选择你自己的冒险风格的游戏,我可能包含一个有我的水平数据的文件 pages.json,粗略地说,我可以说“我知道 pages.json总是存在,并且在我的游戏过程中没有有意义的改变,”所以在这里我允许自己一些杂质: 我将读取该文件与 unsafePerformIO。但是我写的大部分东西,我不会带着直接的副作用去写。在哈斯克尔,为了封装这些副作用,我们做了一些事情,你可以称之为 元编程,用其他程序编写程序ーー但遗憾的是,今天的元编程通常指的是 (用其他源代码重写源代码树) ,它比这种更简单的元编程要强大和危险得多。

Haskell 希望您写出一个纯计算,它将计算一个名为 Main.main的值,该值的类型为 IO (),或者“一个不产生值的程序”程序只是我能在哈斯克尔操作的数据类型之一。然后,Haskell 编译器的工作就是把这个程序作为这个源文件,执行这个纯计算,并把这个有效的程序作为二进制可执行文件放在你的硬盘上的某个地方,以便稍后在你有空的时候运行。换句话说,在纯计算运行(当编译器生成可执行文件时)和有效程序运行(当您运行可执行文件时)之间存在时间差。

对于一个非常轻量级的 TypeScript 类示例(即不是全功能的,也不是可生产的) ,它描述了不可变的程序和一些你可以用它们来做的事情,考虑一下

export class Program<x> {
// wrapped function value
constructor(public readonly 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()));
}
// maybe we also play with overloads and some variable binding
bind<key extends string, y>(name: key, after: Program<y>): Program<x & { [k in key]: y }>;
bind<key extends string, y>(name: key, after: (x: x) => y): Program<x & { [k in key]: y }>;
bind<key extends string, y>(name: key, after: (x: x) => Program<y>): Program<x & { [k in key]: y }>;
bind<key extends string, y>(name: key, after: Program<y> |  ((x: x) => Program<y> | y)): Program<x & { [k in key]: y }> {
return this.chain((x) => {
if (after instanceof Program) return after.map((y) => ({ [name]: y, ...x })) as any;
const computed = after(x);
return computed instanceof Program? computed.map(y => ({ [name]: y, ...x }))
: Program.of({[ name ]: computed as y, ...x });
});
}
}

关键是,如果你有一个 Program<x>,那么没有副作用发生,这些是完全功能纯粹的实体。除非函数不是纯函数,否则在程序上映射函数不会产生任何副作用; 对两个程序进行排序不会产生任何副作用; 等等。

然后,我们的上述函数可能开始写成

function buyCoins(io: IO, user: User, coinPackage: CoinPackage) {
return Program.of({})
.bind('id', io.random.uuidv4)
.bind('exchangeRates', io.biz.getExchangeRates)
.bind('usdEstimate', ({ exchangeRates }) =>
usdFromPrice(coinPackage.price, exchangeRates)
)
.bind(
'createdDate',
io.time.now.map((date) => date.getTime() / 1000)
)
.chain(({ id, usdEstimate, createdDate }) =>
io.biz.saveCoinTransfer({
id,
user,
coinPackage,
state: 'PENDING',
usdEstimate,
createdDate,
})
);
}

重点是,这里的每个函数都是一个完全纯粹的函数; 实际上,甚至 buyCoins(io, user, coinPackage)也是 Program,实际上什么都没有发生,直到我实际上 .run()使它开始运动。

一方面,开始使用这种纯度和抽象级别需要付出很大的代价。另一方面,您可能会看到上面的代码允许轻松地进行模仿——只需将 io参数改为一个以不同方式运行的参数即可。例如,生产值可能看起来像

// module io.time
export now = new Program(async () => new Date());
export sleep = new Program(
(ms: number) => new Promise(accept => setTimeout(accept, ms)));

您可以在一个实际上不睡眠的值中测试 mock,并且在其他情况下具有确定的日期:

function mockIO(): IO {
let currentTime = 1624925000000;
return {
// ...
time: {
now: new Program(async () => new Date(currentTime)),
sleep: (ms: number) => new Program(async () => {
currentTime += ms;
return undefined;
})
}
};
}

在其他语言/框架中,你可以通过大量的反射和自动装配依赖注入来实现这一点; 这些方法可以工作,但是它们需要相当花哨的代码层来实现基本功能; 相比之下,通过定义30行的 class Program<x>所创建的间接性已经足够强大,可以直接允许所有这些模拟,因为我们并不试图使用 注射依赖,而只是使用 提供依赖,这是一个简单得多的目标。

我们能否调用这样的函数纯函数。如果答案是否定的,那么我们如何将其重构为一个?

如你所知,“明天可能会有不同的结果”。如果是这样的话,答案将是一个响亮的 “不”。如果你对 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功能满意的前提下,我们可以得出 fetchFromDatabasedollarToEuro上的投影必须满意的结论。或者“ fetchFromDatabase是纯的”这句话意味着 dollarToEuro是纯的(因为 fetchFromDatabasedollarToEuro环境影响评估基础,是 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)来进行单元测试。

我有一些疑问,把这样一个函数归类为纯函数是否有用,就好像我开始把它和其他“纯函数”一起使用一样,在某个时刻会有一些有趣的行为。

我觉得我更喜欢“纯粹”这个词,因为这意味着我可以在没有意外行为的情况下创作它。

下面是我认为的“功能核心”:

    // 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

如果没有类型,就很难把所有的东西粘在一起。我认为这是某种“最终无标签”和“单子”组合。