这是一个纯函数吗?


117

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

  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)

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

这部分我明白了。


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

(编辑- 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,函数本身不包含任何使输入突变的逻辑,但是它依赖于将来可能会改变的外部常量。在这种情况下,绝对可以每天更改。在其他情况下,可能会发生;可能不会。

我们可以将此类函数称为纯函数吗?如果答案是否定的,那么我们如何将其重构为一个?


6
像JS这样的动态语言的function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
纯净

29
纯度意味着您可以在代码级别用其结果值替换函数调用,而无需更改程序的行为。
鲍勃,

1
去远一点什么构成的副作用,并与更多的理论术语,见cs.stackexchange.com/questions/116377/...
吉尔“SO-停止作恶”

3
今天的功能是(x) => {return x * 0.9;}。明天,您将拥有另一种可能纯净的功能(x) => {return x * 0.89;}。请注意,每次运行(x) => {return x * exchangeRate;}它都会创建一个函数,并且该函数是纯函数,因为exchangeRate无法更改。
user253751 '19

2
这是一个不纯函数,如果要使其纯净,可以将其const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; 用于纯净函数,Its return value is the same for the same arguments.应始终保持1秒,10年..以后不管如何
Vikash Tiwari

Answers:


133

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


6
没有自由变量不是一个函数是纯要求:const add = x => y => x + y; const one = add(42);这里既有add并且one是纯粹的功能。
zerkms

7
const foo = 42; const add42 = x => x + foo;<-这是另一个纯函数,再次使用自由变量。
zerkms

8
@zerkms-我非常希望看到您对这个问题的回答(即使它只是改写使用某些不同术语的SomePerformance)。我认为这不会重复,而且会很有启发性,尤其是在引用时(理想情况下,其来源比上面的Wikipedia文章更好,但是,如果我们能做到的话,那还是个胜利)。(从某种负面的角度看这则评论很容易。相信我,我是真诚的,我认为这样的答案将是不错的,并且想读它。)
TJ Crowder

17
我认为您和@zerkms都是错误的。您似乎认为dollarToEuro答案中示例中的函数不纯,因为它取决于free变量exchangeRate。那太荒谬了。正如zerkms指出的那样,函数的纯度与它是否具有自由变量无关。但是,zerkms也是错误的,因为他认为该dollarToEuro功能不纯,因为它依赖于exchangeRate数据库中的哪个功能。他说这是不纯的,因为“它暂时依赖于IO”。
Aadit M Shah,

9
(续)再次,这很荒谬,因为它表明这dollarToEuro是不纯的,因为它exchangeRate是一个自由变量。它表明如果exchangeRate不是自由变量,即如果它是一个自变量,那么dollarToEuro它将是纯净的。因此,它表明那dollarToEuro(100)是不纯净的,但却dollarToEuro(100, exchangeRate)是纯净的。这显然是荒谬的,因为在两种情况下,您都依赖于exchangeRate数据库中的。唯一的区别是函数exchangeRate内是否有自由变量dollarToEuro
Aadit M Shah,

76

从技术上讲,您在计算机上执行的任何程序都是不纯正的,因为它最终会编译成诸如“将该值移入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));

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

现在,考虑以下功能。

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”。

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功能是纯粹的。


3
这非常有用。谢谢。我确实想const在我的示例中使用。
雪人

3
如果您确实打算使用constdollarToEuro函数,那么该函数确实是纯函数。值exchangeRate更改的唯一方法是再次运行该程序。在这种情况下,旧过程和新过程是不同的。因此,它不会破坏参照透明性。这就像用不同的参数两次调用一个函数。参数可能不同,但是在函数内参数的值保持不变。
Aadit M Shah,

3
这听起来有点像相对论:常数只是相对常数,而不是绝对常数,即相对于运行过程。显然,这里唯一正确的答案。+1。
鲍勃,

5
我不同意“是不纯正的,因为它最终会编译成诸如“将该值移入eax”并将“将该值添加到eax的内容”之类的指令。如果eax通过加载或清除将其清除,则代码将保持确定性,无论还有什么正在发生,因此是纯粹的,否则,非常全面的答案
3Dave

3
@Bergi:实际上,在具有不变值的纯语言中,身份无关紧要。只能通过以下方式之一来观察评估相同值的两个引用是对同一个对象还是对不同对象的两个引用:观察对象通过一个引用进行了变异,然后观察值是否在通过另一个引用进行检索时也发生了变化。没有突变,身份就变得无关紧要。(正如Rich Hickey所说:身份是随着时间流逝的一系列状态。)
JörgW Mittag

23

我纯粹主义者的答案(其中“我”实际上是我,因为我认为这个问题没有一个形式上的形式 “正确”答案):

在像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流不会发生突变)。

换句话说,只关系到函数的行为方式,而不是函数的实现方式。只要一个特定的函数拥有这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

其他相关资源:


4
@TJCrowder me作为zerkms提供了答案。
zerkms,

2
是的,用Javascript就是信心,而不是保证
bob

4
@bob ...或者这是一个阻止呼叫。
zerkms

1
@zerkms-谢谢。如此一来,我100%肯定,您add42和我之间的主要区别addX仅在于x可以更改我,而您ft不能更改(因此,add42的返回值不会根据改变ft)?
TJ Crowder

5
我不同意dollarToEuro您的示例中的函数不纯。我解释了为什么我不同意我的回答。stackoverflow.com/a/58749249/783743
Aadit M Shah,

14

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

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

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

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

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

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

这里的历史权限是您的数据库,并且假设该数据库没有受到损害,则在特定日期始终会返回相同的汇率结果。因此,结合这三个参数,您可以编写general的完全纯净,自给自足的版本,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());

@Snowman不客气!我对答案做了一些更新,以添加更多代码示例。
TheHansinator

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一次查找特定功能或代码块,并在该范围内一致使用相同的汇率。或者,如果仅此线程可以修改数据库,则您有权假定,如果您不更新汇率,则没有其他人可以更改您的汇率。

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

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


8

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

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

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

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

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

示例代码:

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

dollarToEuro(100, fetchFromDatabase())

1
“这几乎肯定会发生变化” ---不是,而是const
zerkms

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")。因此,对to 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” (例如,如果我正在编写字典定义),但是在上下文中将它们视为纯函数没有问题。

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


4

如所写,它是一个纯函数。它不会产生副作用。该函数具有一个形式参数,但具有两个输入,并且对于任何两个输入始终将输出相同的值。


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功能令人满意的前提下,我们可以得出结论,fetchFromDatabaseon 的投影dollarToEuro必须是令人满意的。或声明“ fetchFromDatabase是纯”意味着dollarToEuro是纯粹的(因为fetchFromDatabase依据dollarToEuro通过的换算因子x

从原始帖子中,我可以理解这fetchFromDatabase是一个函数时间。让我们改进重构工作以使这种理解变得透明,从而明确地将其限定fetchFromDatabase为纯函数:

fetchFromDatabase =(timestamp)=> {/ *这里是实现* /};

最终,我将重构功能如下:

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)来进行单元测试。


1
这很有启发性。+1。谢谢。
雪人

虽然我发现您的答案提供了更多信息,但对于的特定用例,也许可以进行更好的重构dollarToEuro。我在OP中提到过,可能还有其他用例。我之所以选择dollarToEuro,是因为它立即唤起了我想要做的事情,但是可能有一些不那么微妙的事情取决于自由变量,该变量可能会改变,但不一定是时间的函数。考虑到这一点,我发现最受好评的重构是一种更易于访问的重构,它可以帮助使用相似用例的其他人。谢谢您的帮助。
雪人

-1

我是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() 某个地方调用该命令,这将是不纯的。但是,您可以通过纯计算来描述整个计算结构,并且可以将杂质推到代码的边缘。


我很好奇为什么这会不断被否决,但是我的意思是我仍然坚持(实际上,这是您在Haskell中默认情况下是纯净的情况下如何操作程序的方式),并且会很高兴地拒绝表决。不过,如果拒绝投票者想发表评论,说明他们对此不满意,我可以尝试对其进行改进。
CR Drost

是的,我想知道为什么作者这么多,但选票这么多,却没有一个评论?
布达厄斯'19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.