百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

代码详解:以forEach为跳板洞悉函数式编程

toyiye 2024-06-28 10:10 12 浏览 0 评论

全文共3388字,预计学习时长7分钟



JavaScript编程员每天都会用多范式语言来处理大量函数式编程工作。那么,你了解多少呢?


一个简单的列表迭代


将下列数据视作为一个字符串列表

const langs = ['lisp', 'haskell', 'ocaml'];


JavaScript并不是强类型语言,因此采用比langs还庞大的集合可能会出现纰漏。鉴于此,我们可对其迭代,并检查控制台,观察所得结果。从迈入编程大门的那一刻起,我们便对该步骤了然于胸。

// imperative good old way
for (let i = 0; i < langs.length; i++) {
 console.log(langs[i]);
}


对我们来说,这条代码看起来清晰易懂,但真是这样吗?


这是命令式语言,因为它描述的是计算机应如何迭代,而非人类自己对迭代的理解。


虽然这条代码命令性比不上带有goto的if语句,但仍属于命令式语言。


除此之外,该代码自身还有几处缺陷:


· 此代码不具有可重用性/不符合DRY原则(即使是相似情形下还是要重写先前所有代码)。

· 鉴于其为程序代码块,该代码既不能分解,也无法重组。

· 维护难度大:也许会有语句排版错误,在循环中倘若i值遭到改变,便会产生程序漏洞。

· 可扩展性差,这是因为嵌套for循环语句足以让每个程序员抓狂不已。


语法糖

多亏了ES6这款对程序员相当友好的算法语言,我们只需使用一些语法糖,无须将额外变量声明为计数器。


// semi declarative syntactic sugar
for (const lang of langs) {
 console.log(lang);
}


现在就更像是个声明式语言了:一个编程小白都能够看懂这串代码,只要从左往右念即可。


虽然可维护性与可扩展性有所提高,但其复杂性仍差强人意。


老当益壮的forEach


早在ES6问世的五年前,JavaScript就已推出一个用处颇大的编程工具:forEach。


forEach既非本地控制结构,也非保留关键字,而是一个数组方法,可在任何数组中执行,就连空数组也不例外。


langs.forEach();


上述例子会导致错误,因为forEach需要一个参数[1]。


但不是某个简单、随机的参数,得是一个函数。


// try to familiarize with this notation
const sayHi = () => alert("hi."); 
// let's use our brand new function
langs.forEach(sayHi)


谨记,函数被尊为JS界的第一等公民。


这并不意味着函数是独一无二的,实际上刚好相反,在JS中,函数与其他数值(对象,本原布尔函数,数字&字符串)地位相当:它们可成为变量,可进行组合,可作为参数传递,可得返回值等。(在Haskell中,就连+也能被作为参数传递)


forEach即函数编程师口中的“高阶函数”,不过也没什么复杂的,不过是一个用于运行并返回其他函数值的函数而已。


// 3 "hello" alerts, one by element
langs.forEach(() => alert('hello'))


因此, forEach 也不是了不得的魔法秘术; 它采用一个声明式函数(未执行!),辅之以下列签名 :(any) -> void[2] 。而后便可将给定函数连续地应用于数组中的每个元素。


该参数any是用来替换迭代中的现有元素, 而void表明该函数不应返回任何数值。


langs.forEach(lang => alert(`hi ${lang}`));


运行上述代码,会跳出一个警示框,内容为 “hi <language>”,针对本数组中的每一语言。这样以来,似乎可根据所学知识解决该问题。


// declarative higher order method
langs.forEach(lang => console.log(lang));


且慢!


console.log 是函数吗?是的。


那 console.log 有 (any) -> void 这样一个签名吗?或许吧[3]。


那么问题来了,这个函数


const log = a => console.log(a); 
log("hi");


和以下这一函数


console.log("hi");


到底有何分别?


似乎看不出任何区别,此包装函数只是噪声污染,所以[4]……


// higher order method enlighten
langs.forEach(console.log);


听上去有些奇怪,但从某种程度上却显得也更加透彻。


这难道是……现在做的就是函数式编程吗?


其实不然,真正的函数式编程极其严格,其中,只允许可证明的程序与可控的副作用存在。若想达到这一目标,需遵循以下规则。


纯函数与不可变性

还记得forEach中所用的函数签名吗?那个函数得不出任何返回值(也就是JS中所谓的未定义),或编程中的“void”。对forEach本身而言,它也是一个“未定义”的方法。这是函数副作用的明显症状,若函数无返回值,那它应该在返回语句发生前,对程序在其他方面上产生影响。例如改变了一些全局变量值,或改变数组本身。


这些事情某天我们可能会将其抛之脑后,但它们终将引发程序漏洞。总之,这些副作用是不可控的,而函数编程师都是控制狂。他们既要知其然,也要知其所以然。


虽然作为一个自定义函数,forEach比for…of语句更具可组合性,但它仍受数组的限制。forEach不能将自行传递给另一种函数。


无论怎么说,可组合性与可扩展性均未实现。


这样一来,想进入FP(函数式编程)的殿堂,就要先定义出一个独具特色的forEach。先用一个全新的函数来隐藏实现细节:


// custom pre-functional for each
const forEach = (arr, func) => {
 for (let i = 0; i < arr.length; i++) {
 func(arr[i]);
 }
};
forEach(langs, console.log);


你可能会说,这明明是命令式语句,小编你个骗纸!


然而,笔者似乎之前并未提到这一方式的具体实现方法。


诚然,这样确实是有瞒天过海之嫌。但如今有了这独家定制的forEach,只要全部编写完毕,就无需为此类函数费心。这样的forEach将前无古人后无来者,起码能用上三十年。函数的真谛,即通过隐藏命令性细节,打造出一个声明性工具。没人让你将先前所学的计算机知识抛之脑后,情况恰恰相反!


即便如此,哪怕不用for语句,我们仍可以完成迭代,这就是递归,不过这是后话,而且没有TCO的话,递归还会带来新的问题[5]。


若想开开眼,可研究下列代码:


// recursive for each
const forEach = (arr, func) => {
 if (arr.length) {
 const [head, ...tail] = arr;
 func(head); 
 forEach(tail, func);
 }
};
forEach(langs, console.log);


接下来是总结时间,关于纯函数,后文还会有所叙述,现在,只需牢记,一个纯函数有两个特点:


· 应该有返回值

· 不能改变所给范围外一切数据,哪怕是给定的引用参数也不例外[6]


// custom functional for each
const forEach = (arr, func) => {
 const newArr = [...arr]; // copy
 for (let i = 0; i < newArr.length; i++) {
 // @see footnote 2
 func(newArr[i], i, newArr); 
 }
 return newArr; // return copy
};
forEach(langs, console.log);


这个forEach看上去真是好多了,可用来甄别与消除副作用。


但显然,代码的可维护性与可扩展性并未得到完全解决。


试问,倘若所有函数均为纯函数,且你的程序均由这些纯函数构成,这样一来易变性又将从何谈起?无从谈起!纯函数的引用透明性,换一种角度来看,恰恰就是其不变性。


科里化


接下来讨论函数式编程另一个有用的工具——科里化。


由于上文已谈到,函数被尊为第一等公民(和其他值类似),因此易知一个函数可以返回另一个函数,并将父变量囊括其中。


const f = a => b => a;
const otherF = f('Hello');
console.log(otherF('Goodbye')); // will log "Hello".
// Play with that until you'll understand why.


这样,一个可重用性与可组合性极佳的forEach就此问世:


// curried pure function
const forEach = func => arr => {
 const newArr = [...arr];
 for (let i = 0; i < newArr.length; i++) {
 func(newArr[i], i, newArr);
 }
 return newArr;
};
const logEach = forEach(console.log);
logEach(langs);
logEach(['see', 'you', 'soon']);
const doubleAndLogEach = 
 forEach(a => console.log(a * 2));
doubleAndLogEach([1, 2, 3]);


注意,此类forEach参数顺序如下所示:


· func, 将被部分应用

· 接着是数列 arr


这种新顺序可以让程序员创造或重写logEach ,甚至是doubleAndLogEach 。未来还可实现forEach与其他函数的组合。


鉴于在JavaScript中,对象均通过引用项传递,因此这款forEach 还有一处较大的纰漏,即包含对象的数列有突变的风险。


JavaScript 既非静态类型语言,也非强类型语言,因此会将不纯函数甚至不良数据类导入 forEach(对于该问题,唯一的应对方式恐怕就是使用TypeScript 或Elm )


因此,理想的完美情形并未实现。读完全文也许你已发现:forEach 本身并非FP向,毕竟定义中也的确暗示了副作用的存在。这也是为何三巨头不包括forEach,而是map, filter, 和reduce。


[1]: 实际上,这还需要第二个参数,this的语境值。

[2]: 然而代码更像是 (any, number, any[]) -> void, 数字即当前索引, any[] 即调用数组,其他的args参数也有用处,例如,若想知道当前元素是否是该数列的最后一个,这些参数便派上了用场。

[3]: 例外:真正的 console.log 签名下,可以包含无限个逗号隔开的可选args参数,并返回错误值 (any, …any[]) -> false.

[?]: 实际上,由于签名存在差别, console.log w呈现的信息会更多(详见备注二与备注三^^).

[?]: 尾调用优化是用递归取代for循坏的先决条件。若采用递归,到最后所调用栈帧可能增长过多,而可调用的栈帧是有限的。TCO不存在于JavaScript中(Node 6除外),因此我们暂时仍困于文中的循环

[?]: 在JavaScript中, 所有对象(包括数组)总是通过引用共享,从不拷贝。你若对此有所疑窦,还需在这方面多下些功夫。


留言 点赞 关注

我们一起分享AI学习与发展的干货

编译组:董宇阳、张婷华

相关链接:

https://medium.com/better-programming/functional-js-from-%CE%B1-to-%CF%89-8dc0cfe1f4e1

如需转载,请后台留言,遵守转载规范

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码