我是Haskell / JS的双语者,Haskell是关于函数纯度的很多语言之一,所以我想我将从Haskell的角度为您提供观点。
正如其他人所说,在Haskell中,读取可变变量通常被认为是不纯的。变量和定义之间的区别在于变量可以稍后更改,定义永远相同。因此,如果您当时就声明了它const
(假设它只是一个a number
并且没有可变的内部结构),那么从中读取将使用纯净的定义。但是您想对随时间变化的汇率建模,这需要某种可变性,然后您会陷入困境。
为了描述Haskell中的那种不纯净的事物(我们可以称其为“效应”,以及它们的使用是“有效的”而不是“纯粹的”),我们进行了您可能称为元编程的事情。今天的元编程通常是指宏,这不是我的意思,而仅仅是编写程序以编写另一个程序的想法。
在这种情况下,在Haskell中,我们编写了一个纯计算,它计算出一个有效的程序,然后该程序将执行我们想要的操作。因此,Haskell源文件(至少是一个描述程序而不是库的文件)的全部要点是描述一个有效的程序的纯计算,该程序会产生空洞,称为main
。然后,Haskell编译器的工作是获取此源文件,执行纯计算,然后将有效的程序作为二进制可执行文件放在硬盘驱动器上的某个位置,以便以后有空运行。换句话说,在纯计算运行的时间(由编译器生成可执行文件的时间)与有效程序的运行时间(无论何时运行可执行文件)的时间之间存在差距。
因此,对我们而言,有效的程序实际上是一种数据结构,它们仅被提及就不会本质上做任何事情(它们的返回值之外没有* side- *效果;它们的返回值包含其效果)。对于一个非常轻量的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()));
}
}
关键是,如果您有a,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()
某个地方调用该命令,这将是不纯的。但是,您可以通过纯计算来描述整个计算结构,并且可以将杂质推到代码的边缘。
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);