【玩转 JS 函数式编程_015】4.2 JavaScript 函数式编程核心基础强化之:玩转非纯函数(Impure functions)

文章目录

  • 4.1 纯函数
    • 4.1.1. 引用透明 Referential transparency
    • 4.1.2. JS 函数中的副作用 Side effects
    • 4.1.3. 纯函数的优势 Advantages of pure functions
  • 4.2. 非纯函数 Impure functions ✔️
    • 4.2.1. 避开非纯函数 Avoiding impure functions
      • 1 避免使用状态 Avoiding the usage of state
      • 2 注入非纯函数 Injecting impure functions
    • 4.2.2. 纯函数的判定 Is your function pure?

(接上篇内容)

4.1 纯函数

4.1.1. 引用透明 Referential transparency

(详见本专栏 【第 012 篇】 )

4.1.2. JS 函数中的副作用 Side effects

(详见本专栏 【第 012 篇】 及 【第 013 篇】)

4.1.3. 纯函数的优势 Advantages of pure functions

(详见本专栏 【第 014 篇】)

4.2. 非纯函数 Impure functions ✔️

如果完全避开各种副作用,那么程序将只能使用硬编码输入,无法显示计算结果;同样,大多数网页都将黯然失色:既无法调用任何 Web 服务,也无法更新 DOM——只有静态页面;而且 Node 代码在服务器端也将毫无意义——因为不执行任何 I/O 操作。

减少副作用是函数式编程一个很好的目标,但过犹不及!接下来,就让我们来考察一下如何避免非纯函数的使用;如果实在避无可避,又该怎样找到最佳方法来圈定或限制其作用范围。

4.2.1. 避开非纯函数 Avoiding impure functions

前面章节介绍了使用非纯函数的一些较常见的原因;本节就来看看,在完全消灭非纯函数不现实的情况下,怎样减少它们在代码中的数量。主要方法有两个:

  • 避免使用状态;
  • 使用一种更通用的注入模式(injection)将非纯函数的行为限制在可控范围内;

1 避免使用状态 Avoiding the usage of state

关于全局状态的使用——获取值也好、设置值也罢,解决方案都是众所周知的。其要点提炼如下:

  • 将全局状态所需的任何内容作为参数传给函数;
  • 如果函数需要更新状态,不宜直接更新,而应该生成一个新状态来作返回值;
  • 应该由调用方接收返回的状态并更新该状态。

这些也是 Redux 中的 reducer 的设计理念。reducer 的签名为 (previousState, action) => newState,表示接收一个状态值 state 和一个 action 作参数,并返回一个新的状态值 newState。更具体地说,reducer 不应该简单地更改 previousState 参数;它必须保持不变(第十章《确保纯度——不变性》将详细介绍)。

最早版本的 isOldEnough 函数用到了一个全局变量 limitYear,改造起来也很简单:将其视为函数参数即可。这样原函数就变成了纯函数,其结果只取决于传入的参数。再进一步,可以将当前年份传入,让函数来处理具体计算,而不是交由调用者亲自计算,代码如下:

const isOldEnough3 = (currentYear, birthYear) => birthYear <= currentYear-18;

很明显,该方案必须更改所有的函数调用,将 limitYear 作为参数传入(也可以使用科里化来解决,第七章《函数转换——科里化与部分应用》会详述)。给 limitYear 赋初值后,和之前一样,其职责依然游离于函数之外,但这样处理已经成功避开了一个陷阱。

不妨在 roundFix 函数上小试牛刀。回忆一下,该函数根据上一次计算的舍入误差的正负,来对累计舍入误差进行修正。虽然无法绕开中间状态,但却可以将当前舍入计算与累计修正计算剥离开。原代码如下:

const roundFix1 = (function() {let accum = 0;return n => {let nRounded = accum > 0 ? Math.ceil(n) : Math.floor(n);accum += n - nRounded;return nRounded;};
})();

新版实现将传入两个参数:

const roundFix2 = (a, n) => {let r = a > 0 ? Math.ceil(n) : Math.floor(n);a += n - r;return {a, r};
};

该函数具体怎么使用呢?其用法是:先初始化累计误差,然后传入该函数,再由调用方完成状态更新:

let accum = 0;// ...some other code...let {a, r} = roundFix2(accum, 3.1415);
accum = a;
console.log(accum, r); // 0.1415 3

注意以下几点:

  • accum 现在是全局状态的一部分;
  • 根据 roundFix2() 的定义,每次调用该函数都会用到当前 accum 的最新的值;
  • 负责更新全局状态的是调用者自己,而不是 roundFix2() 函数本身。

提示

注意第 5 行解构赋值的用法,可以将函数返回一个以上的结果并轻松赋给不同的变量。更多详情,参考 MDN 官方文档。

改造后的新 roundFix2() 函数就是百分百的纯函数了,测试起来也会很轻松。若要对程序其他部分隐藏 accum,可以继续使用闭包,但这样会由于函数自身的调用而再次引入不纯的代码。

2 注入非纯函数 Injecting impure functions

如果一个函数因为调用了另一个不纯的函数而变得不纯,其中一种解题思路,是将所需函数直接注入原函数。该方案提供了更大的代码灵活性,应对后续变动更轻松,进行单元测试也更简单。

再来回顾一下用于生成随机文件名的示例函数。原函数的问题在于,使用了 getRandomLetter() 函数来生成文件名(第 5 行):

const getRandomFileName = (fileExtension = "") => {const NAME_LENGTH = 12;let namePart = new Array(NAME_LENGTH);for (let i = 0; i < NAME_LENGTH; i++) {namePart[i] = getRandomLetter();}return namePart.join("") + fileExtension;
};

解决该问题的一个思路是用一个外部注入的函数来替换非纯函数;此时需要为之前的 getRandomFileName 函数引入一个新的参数 randomLetterFunc

const getRandomFileName2 = (fileExtension = "", randomLetterFunc) => {const NAME_LENGTH = 12;let namePart = new Array(NAME_LENGTH);for (let i = 0; i < NAME_LENGTH; i++) {namePart[i] = randomLetterFunc();}return namePart.join("") + fileExtension;
};

这样就从原函数中移除了原来的非纯函数行为。虽然引入的是一个给定的伪随机函数,但如果返回的一个是固定的、已知的值,那么后续就可以轻松对该函数进行单元测试;后续的示例还将进一步演示具体做法。函数的用法已然改变,此时需要这么写:

let fn = getRandomFileName2(".pdf", getRandomLetter);

如果看上去不太习惯,也可以处理成给 randomLetterFunc 参数提供一个默认值,如下所示(注意第 3 行):

const getRandomFileName2 = (fileExtension = "",randomLetterFunc = getRandomLetter
) => {...
};

当然,这个问题也可以通过部分传参(partial application)来解决,后续第 7 章《函数变换——柯里化及部分传参技术》会进一步介绍。

其实这样做并没有完全避免使用非纯函数。正常情况下,我们还是会通过传入的随机字母生成逻辑来调用 getRandomFileName(),因此改造后的函数本质上仍然是非纯函数。但是,出于测试方面的考虑,如果传入的是一个返回给定值而非随机值的函数,就可以假定它是个纯函数来进行测试,操作起来就会方便很多。

而对于这一切的始作俑者、非纯函数 getRandomLetter() 又该如何处置呢?可以使用同样的技巧,引入一个参数来生成随机数,从而得到下面的函数新版本(留意第 1 行):

const getRandomLetter = (getRandomInt = Math.random) => {const min = "A".charCodeAt();const max = "Z".charCodeAt();return String.fromCharCode(Math.floor(getRandomInt() * (1 + max - min)) + min);
};

对于常规调用,getRandomFileName() 中的 getRandomLetter() 在调用时不带任何参数,也就是说 getRandomLetter() 将按照默认的随机逻辑进行计算;但若要测试该函数是否按预期运行,则可以人为注入一个伪随机函数来进行测试,让该函数返回测试预设的结果,从而充分测试原函数。

这一思路至关重要,在解决其他类似的问题时也有相当广泛的应用。例如,可以提供某个注入函数(injected functions),避免在原函数中直接访问 DOM。这样在后续测试时,要验证被测试函数是否确实完成了它要实现的功能,将会变得非常简单,无需真正与 DOM 进行交互(当然,测试那些与 DOM 相关的注入函数还需要其他的一些方法,但不是此处的重点)。类似的操作也适用于需要更新 DOM、生成新元素以及进行各种 DOM 操作的其他函数——只需引入一些中介函数即可。

4.2.2. 纯函数的判定 Is your function pure?

让我们通过考虑一个重要的问题来结束本小节的学习:您如何确保一个函数确实是纯函数?为演示该问题的难点,不妨再来看看第 1 章中介绍过了一个简单求和的函数 sum3()。简便起见,这里改写为箭头函数的形式。问题来了:该函数是纯函数吗?看起来确实像:

const sum3 = (x, y, z) => x + y + z;

让我们考察考察:这个 sum3 函数除了访问其自身的参数外,不涉及外部任何内容,也没有中途修改参数的迹象(暂且不论能不能改(要是能改,情况又如何?));既不执行任何 I/O 操作,也不与之前提到的任何非纯函数或非纯方法打交道。这么一来,判定为纯函数会有什么问题吗?

问题的关键在于你的前提条件。举个例子,谁说 sum3 的参数就一定是数字?您可能会做出让步:好吧,就当它们也可以是字符串吧;可即便如此,函数也仍然是纯函数啊,不是吗?但是(用这种语气说明肯定不对),如果是下面这种情况,答案还会是纯函数吗:

let x = {};
x.valueOf = Math.random;let y = 1;
let z = 2;console.log(sum3(x, y, z)); // 3.2034400919849431
console.log(sum3(x, y, z)); // 3.8537045249277906
console.log(sum3(x, y, z)); // 3.0833258308458734

高手支招

注意,上述代码将一个新函数赋给了 x.valueOf 方法。这是对“函数是一等对象”这一知识点的灵活应用。更多相关介绍,详见本书第三章第 3.1.3 节《不必要的错误(An unnecessary mistake)》。

由此可见,sum3() 是不是纯函数,其实取决于传给它的参数是什么;在 JavaScript 中,是完全可以让一个纯函数以非纯函数的方式来运行的!您可能会自我安慰说,肯定不会遇到这样传参的情况,但这些边缘情况(edge cases)往往就是 Bug 的藏身之所;但也不必放弃使用纯函数的想法。尽管 JavaScript 无法让您完全确信写出的代码 始终是纯函数形式 的,但至少只要通过添加一些类型检查(比如使用第 1 章介绍转译工具时提过的 TypeScript)来有效防止上述情况的发生。

通过前面几节的介绍,想必您以及了解了纯函数和非纯函数各自的特点。在本章的最后一节,让我们再来看看如何测试所有这些函数。

(本节完)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/56263.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux源码安装slurm以及mung和openssl

一、源码安装munge 1、编译安装munge &#xff08;1&#xff09;下载munge地址&#xff1a;https://github.com/dun/munge/releases &#xff08;2&#xff09;解压编译安装&#xff1a; 1 2 3 4 5 6 7 8 创建/data目录 复制文件munge-0.5.15.tar.xz 到/data目录下 tar -Jx…

vue3日历组件库Vue-Cel使用

官网地址 安装 npm i vue-cal<template><div class"calendar-container"><!-- 这个日历双击可以跳转到下一个周期,但是我项目不允许点击,没找到相关的禁用事件,这里写个蒙层阻止点击 --><div class"mask"></div><!-- 日…

STM32单片机(F03C8T6)-点灯(寄存器点灯和库函数点灯)

作者&#xff1a;Whappy 时间&#xff1a;2024.10.9 日志&#xff1a;本实验是我第一个32位单片机实验程序&#xff0c;如有不足请指点&#xff01; #include "stm32f10x.h" // Device headerint main(void) {//寄存器点灯RCC->APB2ENR 0x000…

界面耻辱纪念堂--可视元素03

更多的迹象表明&#xff0c;关于在程序里使用新的动态界面元素&#xff0c;微软的态度是不确定的&#xff0c;其中一个是仅仅需要对比一下Office97 里的“Coolbars”和“标准工具条”。Coolbar 按钮直到用户指针通过的时候才成为按钮&#xff08;否则是平的&#xff09;。 工具…

Monorepo pnpm 模式管理多个 web 项目

Monorepo pnpm 模式管理多个 web 项目 Monorepo pnpm 模式管理多个 web 项目项目地址git flow 工作流程pnpm workspace.npmrc初始化项目架构引入Husky规范git提交配置eslint和prettiereslint 配置prettier 配置 配置lint-staged创建项目创建shared项目全局安装 vue在 packages …

新兴的安全职业挑战

我们经常与安全专业人士交谈&#xff0c;他们希望在努力提升职业发展的同时提高自己的价值并克服组织内部的挑战。在这些谈话中&#xff0c;花费大量时间讨论公司未来将面临的安全问题并不罕见。 安全领导者希望为问题制定计划并获得领导层对其计划的支持。这通常意味着实施修…

025 elasticsearch索引管理-Java原生客户端

文章目录 pom.xml1创建索引2.创建索引并设置settings信息3.创建索引并设置mapping信息4.删除索引库5.给未设置mapping的索引设置mapping elasticsearch版本7.10.2&#xff0c;要求java客户端与之相匹配&#xff0c;推荐Springboot版本是2.3以上版本 依赖配置使用的是JUnit 5&am…

oracle操作回退SQL

对表数据进行操作后&#xff0c;提交了事务&#xff0c;发现数据更新或者删除错了&#xff0c;这时候还是有方法可以数据回退的 进行数据回退操作&#xff0c;将数据回退到xxxx分钟&#xff0c;也就是update操作之前&#xff1a; 在回退之前&#xff0c;先查询xx分钟的数据&…

MYSQL-windows安装配置两个或多个版本MYSQL

安装第一个mysql很简单&#xff0c;这里不再赘述。主要说说第二个怎么安装&#xff0c;服务怎么配置。 1. 从官网下载第二个MySQL并安装 一般都是免安装版了&#xff0c;下载解压到某个文件目录下(路径中尽量不要带空格或中文)&#xff0c;再新建一个my.ini文件&#xff08;或…

Reality Capture 软件安装 附下载链接

Reality Capture 软件安装 文章目录 Reality Capture 软件安装一、Reality Capture v1.4汉化版安装包下载并解压二、Epic Games Launcher安装三、设置路径并安装![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f077210990674d9fa9c10b52338b52fe.png)四、启动Epic Ga…

Chromium 如何查找V8 引擎中JavaScript 标准内置对象

JavaScript 标准内置对象 - JavaScript | MDN (mozilla.org) 一、JavaScript 标准内置对象 本章介绍和说明了 JavaScript 中所有的标准内置对象、以及它们的方法和属性。 这里的术语“全局对象”&#xff08;或标准内置对象&#xff09;不应与 global 对象混淆。这里的“全局…

OpenStack服务Swift重启失效(已解决)

案例分析Swift重启失效 1. 报错详情 在重新启动 VMware 虚拟机后&#xff0c;我们发现 OpenStack 的 Swift 服务出现了 503 Service Unavailable 错误。经过排查&#xff0c;问题根源在于 Swift 服务所使用的存储挂载是临时挂载&#xff0c;而非永久挂载。 Swift 服务依赖于…

985研一学习日记 - 2024.10.11

偶尔一碗热鸡汤&#xff1a;一个人内耗&#xff0c;说明他活在过去&#xff1b;一个人焦虑&#xff0c;说明他活在未来。只有当一个人平静时&#xff0c;他才活在现在。 日常 1、6&#xff1a;00起床 √ 2、健身1h 今天练了肩部以及背&#xff0c;然后跑步半小时 3、LeetC…

Python环境搭建

Python环境搭建 1 安装Python 双击安装包 2 安装Pycharm 双击安装包,然后下一步 安装完成后重启电脑 3 破解Pycharm 启动Pycharm 破解 4 pycharm的基本配置 1.主题颜色 2. 文件语言编码 滚动鼠标滑轮ctrl&#xff0c;可以快速缩放文本字体大小。 控制台编码的格式&#x…

Qt - QMenu

QMenu 1、menu转string输出 //GlobalEnum.h #include <QObject> #include <QMetaEnum> class GlobalEnum : public QObject {Q_OBJECT public:EnumTest();enum Enum_Test{ZhangSan 0,WangWu,};Q_ENUM(Enum_Test) };#define EnumToString(e) \ QMetaEnum::fromTy…

Python异步编程:使用`asyncio.Semaphore`控制并发

Python异步编程&#xff1a;使用asyncio.Semaphore控制并发 1. 什么是asyncio.Semaphore&#xff1f;2. 为什么需要asyncio.Semaphore&#xff1f;3. 如何使用asyncio.Semaphore&#xff1f;3.1 基本用法3.2 信号量的工作原理3.3 动态调整信号量 4. 适用场景5. 总结 在Python的…

Qt初识_通过代码创建hello world

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Qt初识_通过代码创建hello world 收录于专栏【Qt开发】 本专栏旨在分享学习Qt的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1.通过按…

c语言库文件

c语言库文件 动态库动态库制作首先生成动态库文件名使用动态库编译动态库运行删除动态库1.sudo su 静态库静态库制作需要将源文件.c转换成二进制文件.o&#xff08;重定向文件&#xff09;将重定向文件打包成静态库文件使用静态库文件 静态库和动态库的区别 库文件的概念 我们将…

python-读写Excel:openpyxl-(1)基础操作之读写数据

目录 工作薄 创建/打开工作薄对象 获得默认sheet 工作表 创建工作表 删除工作表 获取表名 复制表 获取表对象 单元格 列号数字与字母转换 获取有效数据行和列 单元格对象获取 获取有效数据区域 获取指定行对象 value获取单元格数据值 修改及写入数据 插入或删…

C#里使用DataGridView来显示500万行数据

如果有500万行数据需要显示,大家肯定感觉很麻烦。 因为一次性地往列表里添加500万行数据,就需要很久的时间, 并且当滚动的时候就会更新比较慢。 有没有更好的方式呢? 其实可以采用虚拟列表的方式,也就是设置下面的方式: dataGridView1.VirtualMode = true; 这样列表…