WEB程序员笔记

一个前端开发工程师的个人博客

处理javascript中的副作用和纯函数

您听说过“纯功能”一词吗?那“副作用”呢?如果那时您可能已经听说过副作用是有害的,应该不惜一切代价避免(就像一样var。)这是问题所在,如果您编写JavaScript,则可能要引起这些副作用(特别是如果您获得了报酬,因此,解决方案不是避免所有副作用,而是要控制它们。我将向您展示一些您可以做的事情,以使您的纯函数和副作用都很好。

在开始之前,让我们对一些术语进行一些回顾,以便我们都可以进入同一页面。

概念

纯功能

为了简单起见,让我们说一个纯函数是一个函数,其输出仅由其输入确定,并且对外部世界没有可观察的影响。他们提供的主要好处(在我看来)是可预测性,如果您给他们相同的输入值,它们将始终为您返回相同的输出。让我们看一些例子。

这是纯净的。

function increment(number) {
  return number + 1;
}

这不是

Math.random();

这些都是棘手的。

const A_CONSTANT = 1;

function increment(number) {
  return number + A_CONSTANT;
}

module.exports ={
  increment
};
function a_constant() {
  return 1;
}

function increment(number) {
  return number + a_constant();
}

副作用

我会称其为损害功能“纯净性”的任何副作用。该列表包括但不限于:

  • 以任何方式更改(变异)外部变量。
  • 在屏幕上显示事物。
  • 写入文件。
  • 发出http请求。
  • 产生一个过程。
  • 将数据保存在数据库中。
  • 调用具有副作用的其他函数。
  • DOM操作。
  • 随机性。

因此,任何可以改变“世界状况”的行动都是副作用。

我们如何一起使用这些东西?

您可能仍在考虑该副作用列表,从根本上讲,javascript是所有有用的东西,但是有些人仍然告诉您要避免使用它们。不要担心我会提出建议。

好的旧功能组成

另一种说法是:旧的关注点分离。这是非复杂的方法。如果有办法将计算与副作用分开,则将计算放在函数上,然后将输出提供给具有副作用的函数/块。

做这样的事情可能很简单。

function some_process() {
  const data = get_data_somehow();
  const clean_data = computation(data);
  const result = save(clean_data);

  return result;
}

现在,some_process仍然不是纯洁的,但是没关系,我们正在编写javascript,我们不需要所有的东西都是纯洁的,我们需要保持理智。通过将纯计算的副作用分开,我们创建了三个独立的函数,一次只能解决一个问题。您甚至可以更进一步,并使用管道之类的辅助函数来摆脱那些中间变量并直接组成这些函数。

const some_process = pipe(get_data_somehow, computation, save);

但是现在我们又产生了另一个问题,当我们想要在其中一个副作用中发生副作用时会发生什么?我们做什么?好吧,如果一个辅助函数引起了问题,那么我说使用另一个辅助函数来摆脱它。这样的事情会起作用。

function tap(fn) {
  return function (arg) {
    fn(arg);
    return arg;
  }
}

这将使您将具有副作用的功能放置在功能链的中间,同时保持数据流。

const some_process = pipe(
  get_data_somehow,
  tap(console.log),
  computation,
  tap(a_side_effect),
  save
);

有人反对这种类型的事情,有人会争辩说,现在您的所有逻辑都散布在各处,您必须四处走动才能真正知道函数的功能。我真的不介意,这是个人喜好问题。但是,关于它tap的签名就足够了,让我们来谈谈其签名,看看tap(fn)它需要一个回调作为参数,让我们看看如何利用它来发挥我们的优势。

让别人来处理问题

众所周知,生活并不总是那么简单,有时我们只是无法做出如此甜美的功能。在某些情况下,我们需要在流程的中间做一些副作用,而一旦发生,我们总是会作弊。在javascript中,我们可以将函数视为值,这使我们可以做一些有趣的事情,例如将函数作为参数传递给其他函数。这样,当我们需要时,该功能可以灵活地执行副作用,同时保持我们已知和喜爱的一些可预测性。

举例来说,假设您有一个已经是纯函数并且可以对数据集进行某些操作的函数,但是由于某种原因,现在您需要在转换发生后立即记录原始值和转换后的值。您可以做的就是添加一个函数作为参数,并在适当的时候调用它。

function transform(onchange, data) {
  let result = Array.isArray(data) ? [] : {};
  for(let key in data) {
    result[key] = data[key] + 1;
    onchange(data[key], result[key]);
  }

  return result;
}

从技术上讲,它满足了纯函数的某些要求,该函数的输出(和行为)仍由其输入决定,碰巧这些输入之一就是可以触发任何副作用的函数。同样,此处的目标不是要与javascript的性质作斗争,而是要使所有内容均为100%纯,我们希望控制副作用的发生时间。因此,在这种情况下,控制是否有副作用的人是该函数的调用者。这样做的另一个好处是,如果您想在单元测试中使用该函数来证明仍然可以按预期运行,那么您唯一需要做的就是提供其参数,而无需获取任何模拟库来进行测试它。

您可能想知道为什么将callback作为第一个参数,这实际上与个人喜好有关。如果将thing更改最频繁地放在最后一个位置,则可以更容易地进行部分应用,即在不执行函数的情况下绑定参数的值。例如,您可以transform.bind用来创建已经具有onchange回调的专用函数。

懒惰的影响

这里的想法是延迟不可避免的事情。不是立即执行副作用,而是为函数的调用者提供了一种在他们认为合适时执行副作用的方法。您可以通过以下两种方式进行操作。

使用函数包装器

正如我之前在javascript中提到的那样,您可以将函数视为值,并且可以使用值做的一件事是从函数中返回它们。我说的是返回函数的函数。我们已经看到了可能有用的东西,如果您考虑的不是那么疯狂,您有多少次看到这样的东西?

function Stuff(thing) {

  // setup

  return {
    some_method() {
      // code...
    },
    other() {
      // code...
    }
  }
}

这是老派的“建设者”。以前,在ES5好的今天,这是模拟类的一种方法。是一个返回对象的常规函数​​,我们都知道对象可以有方法。我们想要做的就是这样,我们想要将包含副作用的模块转换为一个函数并返回它。

function some_process(config) {

  /*
   * do some pure computation with config
   */

  return function _effect() {
   /*
    * do whatever you want in here
    */ 
  }
}

这样,我们为函数的调用者提供了在需要时使用副作用的机会,他们甚至可以传递副作用并将其与其他函数组合。有趣的是,这不是一个非常常见的模式,可能是因为还有其他方法可以实现相同的目标。

使用数据结构

创建惰性效果的另一种方法是将副作用包装在数据结构中。我们要做的是将效果视为常规数据,能够操纵它们,甚至以安全的方式链接其他效果(我的意思是不执行它们)。您可能之前已经看过这一点,我能想到的一个例子是Observables。看一下使用rxjs的代码。

// taken from:
// https://www.learnrxjs.io/operators/creation/create.html

/*
  Increment value every 1s, emit even numbers.
*/
const evenNumbers = Observable.create(function(observer) {
  let value = 0;
  const interval = setInterval(() => {
    if (value % 2 === 0) {
      observer.next(value);
    }
    value++;
  }, 1000);

  return () => clearInterval(interval);
});

结果Observable.create不仅延迟的执行,setInterval而且使您能够调用evenNumbers.pipe以链接也可能具有其他副作用的其他可观察对象。现在当然不是Observable和rxjs了,我们可以创建自己的效果类型。如果要创建一个,我们只需要一个函数来执行效果,另一个函数就可以组成效果。

function Effect(effect) {
  return {
    run(...args) {
      return effect(...args);
    },
    map(fn) {
      return Effect(arg => fn(effect(arg)));
    }
  };
}

它看起来可能不多,但实际上足以使用。您可以开始合成效果而不会触发环境的任何更改。您现在可以做这样的事情。

const persist = (data) => {
  console.log(`saving ${data} to a database...`);
  return data.length ? true : false;
};
const show_message = result => result 
  ? console.log('we good') 
  : console.log('we not good');

const save = Effect(persist).map(show_message);

save.run('some stuff');
// saving some stuff to a database...
// we good

save.run('');
// saving  to a database...
// we not good 

如果您曾经习惯于Array.map进行数据转换,那么使用时Effect,您会感到宾至如归,您所要做的就是提供具有副作用的功能,并且在链的最后,您Effect将知道准备好要做什么时该怎么做。叫它。

我只是从头开始介绍您可以使用的功能Effect,如果您想了解更多信息,请尝试搜索术语functorIO Monad,我保证您会很开心的。

现在怎么办?

现在,您单击帖子末尾的链接,这是一篇非常不错的文章(基本上是该文章的更好版本)。

我希望现在您有足够的信心开始在代码中编写纯函数,并将它们与javascript可以实现的便捷副作用结合起来。

资料来源

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注