javascript - 这是一个纯函数吗?




function functional-programming (7)

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

正如您适当指出的那样, “明天可能会给我带来不同的输出” 。 在这种情况下,答案将是巨大的 “否” 。 如果您对 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 是纯净的(因为 fetchFromDatabasex 的标量因数对 dollarToEuro 基础

从原始帖子中,我可以了解到 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();

因此,可以通过简单地证明 fetchFromDatabase 正确调用 fetchFromDatabase (或其派生的 exchangeRate )来进行单元测试。

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

  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 在不经意间使用过。)

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

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


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

在像JS这样的动态语言中,有很多可能性可以修补基本类型,或者使用 Object.prototype.valueOf 之类的功能来构成自定义类型, 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

我实用主义者的回答:

sources

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

  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://.com/a/58749249/251311 : https://.com/a/58749249/251311

其他相关资源:


从技术上讲,您在计算机上执行的任何程序都是不纯正的,因为它最终会编译为“不纯”的指令,例如“将该值移入 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 可能会不同意我的看法。 他在 answer 说,以下示例中的 dollarToEuro 函数是不纯的,因为“它 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 函数是纯函数。


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


我想从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)dollarToEuro(100) 的第一次调用的结果,并优化掉第二次调用。 结果将是相同的,因此我们只能记住以前的值。

在调用查找它的任何函数之前,可能只设置一次 exchangeRate ,并且永远不要修改它。 限制较少,您可能拥有的代码会针对特定功能或代码块查找一次 exchangeRate ,并在该范围内一致使用相同的汇率。 或者,如果仅此线程可以修改数据库,则您有权假定,如果您不更新汇率,则没有其他人可以更改您的汇率。

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

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


正如其他人所说,读取可变变量通常被认为是不纯的。 如果您已将其声明为 const 那么(假设它只是一个 number 并且没有可变的内部结构)将是纯净的。

为了将这样的代码重构为纯粹的函数式编程语言(例如Haskell),我们进行了 元编程 ,也就是说,我们编写了处理不可变程序的代码。 这些程序实际上并不执行任何操作,而是简单地存在。

因此,对于一个非常轻量级的类示例,它描述了不可变程序以及您可以使用它们进行的一些操作,

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() 启动它 之前,实际上什么都没有发生 。

同样,我们可以有

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 现在也是从数字到生成数字的程序的纯函数,并且您可以以确定性的方程方式进行推理,从而可以推理出没有副作用的任何程序。


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





functional-programming