前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >谈谈 JavaScript 纯函数

谈谈 JavaScript 纯函数

作者头像
Leophen
发布2021-08-06 11:35:08
4850
发布2021-08-06 11:35:08
举报
文章被收录于专栏:Web前端开发Web前端开发

一、什么是纯函数

纯函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

比如 slicesplice,这两个函数的作用并无二致——但它们各自的方式却大不同。 其中,slice 符合纯函数的定义,因为对相同的输入它保证能返回相同的输出:

代码语言:javascript
复制
var xs = [1, 2, 3, 4, 5];

// 纯的
xs.slice(0, 3);
//=> [1,2,3]

xs.slice(0, 3);
//=> [1,2,3]

xs.slice(0, 3);
//=> [1,2,3]

splice 却会嚼烂调用它的那个数组,然后再吐出来,这就会产生可观察到的副作用,即原数组永久地改变了:

代码语言:javascript
复制
var xs = [1, 2, 3, 4, 5];

// 不纯的
xs.splice(0, 3);
//=> [1,2,3]

xs.splice(0, 3);
//=> [4,5]

xs.splice(0, 3);
//=> []

在函数式编程中,我们追求的是那种可靠的,每次都能返回相同结果的纯函数,而不是像 splice 这样每次调用后都把数据弄得一团糟的函数。

来看看另一个例子:

代码语言:javascript
复制
// 不纯的
var minimum = 21;
var checkAge = function (age) {
  return age >= minimum;
};


// 纯的
var checkAge = function (age) {
  var minimum = 21;
  return age >= minimum;
};

在不纯的版本中,checkAge 的结果将取决于 minimum 这个可变变量的值。 换句话说,它取决于系统状态(system state),这便引入了外部的环境,增加了认知负荷(cognitive load),而使用纯函数的形式,函数就能做到自给自足。

另外,我们可以让 minimum 成为一个不可变(immutable)对象,这样就能保留纯粹性,因为状态不会有变化。要实现这个效果,必须得创建一个对象,然后调用 Object.freeze 方法:

代码语言:javascript
复制
var immutableState = Object.freeze({
  minimum: 21
});

二、副作用

我们在纯函数定义中提到的副作用是什么呢?

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用包括但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

概括来讲,只要是跟函数外部环境发生的交互就都是副作用。 函数式编程的哲学就是假定副作用是造成不正当行为的主要原因,这并不是说要禁止使用一切副作用,而是要让它们在可控的范围内发生。

副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数需根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,则无法保证这一点。

下面通过数学中的函数来了解下为何要坚持「相同输入得到相同输出」原则:

数学中函数的定义:

函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。

换句话说,函数只是两种数值之间的关系:输入和输出。 这是一个合法的从 xy 的函数关系:

下面这种就不是一种函数关系,因为输入值 5 指向了多个输出:

纯函数就是数学上的函数,而且是函数式编程的全部,使用这些纯函数编程能够带来大量好处。

三、纯函数好在哪里

1、可缓存性(Cacheable)

首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:

代码语言:javascript
复制
var squareNumber = memoize(function (x) { return x * x; });

squareNumber(4);
//=> 16

squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16

squareNumber(5);
//=> 25

squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25

下面代码是一个简单的实现:

代码语言:javascript
复制
var memoize = function (f) {
  var cache = {};

  return function () {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

值得注意的是,可以通过延迟执行的方式把不纯的函数转换为纯函数:

代码语言:javascript
复制
var pureHttpCall = memoize(function (url, params) {
  return function () { return $.getJSON(url, params); }
});

这里并没有真正发送 http 请求——只是返回了一个函数,当调用它时才会发请求。 这个函数之所以是纯函数,是因为它总是会根据相同的输入返回相同的输出:给定了 urlparams 之后,它就只会返回同一个发送 http 请求的函数。

这个 memoize 函数工作起来没有任何问题,虽然它缓存的并不是 http 请求所返回的结果,而是生成的函数。

这种方式可以缓存任意一个函数,不管它们看起来多么具有破坏性。

2、可移植性/自文档化(Portable / Self-Documenting)

纯函数是完全自给自足的,它需要的所有东西都能轻易获得。 这种自给自足的好处是什么呢? 首先,纯函数的依赖很明确,因此更易于观察和理解——没有偷偷摸摸的小动作:

代码语言:javascript
复制
// 不纯的
var signUp = function (attrs) {
  var user = saveUser(attrs);
  welcomeUser(user);
};

var saveUser = function (attrs) {
  var user = Db.save(attrs);
    ...
};

var welcomeUser = function (user) {
  Email(user, ...);
    ...
};

// 纯的
var signUp = function (Db, Email, attrs) {
  return function () {
    var user = saveUser(Db, attrs);
    welcomeUser(Email, user);
  };
};

var saveUser = function (Db, attrs) {
    ...
};

var welcomeUser = function (Email, user) {
    ...
};

这个例子表明,纯函数对于其依赖必须要诚实,这样我们就能知道它的目的。仅从纯函数版本的 signUp 的签名可以看出,它将要用到 DbEmailattrs,这在最小程度上给了我们足够多的信息。

相比不纯的函数(难以知道它们暗地里做了什么),纯函数能够提供多得多的信息。

在 JavaScript 的设定中,可移植性可以意味着把函数序列化(serializing)并通过 socket 发送。也可以意味着代码能够在 web workers 中运行。总之,可移植性是一个非常强大的特性。

命令式编程中“典型”的方法和过程都深深地根植于它们所在的环境中,通过状态、依赖和有效作用(available effects)达成;纯函数与此相反,它与环境无关,可以在任何地方运行它。

3、可测试性(Testable)

纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。

Quickcheck,一个为函数式环境量身定制的测试工具。

4、合理性(Reasonable)

纯函数最大的好处是引用透明性(referential transparency)。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。

由于纯函数总是能够根据相同的输入返回相同的输出,所以能够保证总是返回同一个结果,这也就保证了引用透明性。我们来看一个例子:

代码语言:javascript
复制
var Immutable = require('immutable');

var decrementHP = function (player) {
  return player.set("hp", player.hp - 1);
};

var isSameTeam = function (player1, player2) {
  return player1.team === player2.team;
};

var punch = function (player, target) {
  if (isSameTeam(player, target)) {
    return target;
  } else {
    return decrementHP(target);
  }
};

var jobe = Immutable.Map({ name: "Jobe", hp: 20, team: "red" });
var michael = Immutable.Map({ name: "Michael", hp: 20, team: "green" });

punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})

decrementHPisSameTeampunch 都是纯函数,所以是引用透明的。我们可以使用一种叫做“等式推导”(equational reasoning)的技术来分析代码。所谓“等式推导”就是“一对一”替换,有点像在不考虑程序性执行的怪异行为(quirks of programmatic evaluation)的情况下,手动执行相关代码。我们借助引用透明性来剖析一下这段代码。

首先内联 isSameTeam 函数:

代码语言:javascript
复制
var punch = function (player, target) {
  if (player.team === target.team) {
    return target;
  } else {
    return decrementHP(target);
  }
};

因为是不可变数据,我们可以直接把 team 替换为实际值:

代码语言:javascript
复制
var punch = function (player, target) {
  if ("red" === "green") {
    return target;
  } else {
    return decrementHP(target);
  }
};

if 语句执行结果为 false,所以可以把整个 if 语句都删掉:

代码语言:javascript
复制
var punch = function (player, target) {
  return decrementHP(target);
};

如果再内联 decrementHP,我们会发现这种情况下,punch 变成了一个让 hp 的值减 1 的调用:

代码语言:javascript
复制
var punch = function (player, target) {
  return target.set("hp", target.hp - 1);
};

总之,等式推导带来的分析代码的能力对重构和理解代码非常重要。

5、并行代码

可以并行运行任意纯函数,因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

并行代码在服务端 js 环境以及使用了 web worker 的浏览器中很容易实现,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-07-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是纯函数
  • 二、副作用
    • 数学中函数的定义:
    • 三、纯函数好在哪里
      • 1、可缓存性(Cacheable)
        • 2、可移植性/自文档化(Portable / Self-Documenting)
          • 3、可测试性(Testable)
            • 4、合理性(Reasonable)
              • 5、并行代码
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档