函数式编程及其在前端的应用
函数式编程近几年在前端开发中非常火,那么到底什么是函数式编程?这种模式相比于其他模式又有什么优势呢?本文从五个方面入手,探讨一下函数式编程,从数学思维上解释为什么要用纯函数,要有数据不可变性,要控制副作用。
概述:
- 函数式编程起源和概念
- 函数式编程的特性
- 函子的简单介绍
- 函数式编程的优势
- 函数式编程在前端的应用
一、函数式编程的起源和概念
1,起源
函数是数学中非常重要的概念,程序中所用到的函数和数学也是分不开的。函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。
1.1范畴论
范畴就是使用箭头连接的物体,也就是有关联的物体。
范畴论是一门数学学科,范畴是指彼此之间存在某种关系的概念、事物、对象等等,范畴成员之间称之为态射。
数学模型
范畴论是集合论更上层的抽象,简单的理解就是"集合 + 函数"。
范畴与容器
可以把"范畴"想象成是一个容器,容器中有两样东西,一个是值,一个是变形关系函数
1.2 范畴论与函数式编程的关系
范畴中使用函数表达成员之间的关系,随着这门学科的发展,就产生了一整套函数的运算方法,在计算机硬件发展到一定阶段时,这套理论在计算机中得以实现,逐步变成了今天的函数式编程
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
1.3 数学到编程的限制
由于函数式编程从本质上讲是数学运算,原始目的是求值,不做其他事情,这也就是函数式编程为什么要求使用纯函数来编写。否则,就违背了函数最初的运算法则。
2,概念
维基百科定义:
函数式编程(英语:functional programming),又称泛函编程,是一种编程范式,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用
2.1 前端中函数式编程的定义
在前端开发中,主要开发语言是javascript,而js自身并不是函数式编程语言,也就没有一个明确的定义。
在前端开发中,通常使用FP 通过声明纯函数抽象数据的处理,来避免或尽可能减少函数调用对于外部状态和系统产生的副作用,这样一种思想称为函数式编程
二、函数式编程的特性
1,函数是'一等公民'
函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值
2,闭包和高阶函数
函数编程支持函数作为第一类对象,有时称为闭包或者仿函数(functor)对象。实质上,就是用闭包来替代对象,存储变量。
由于函数是一等公民,函数也可以当作参数传递,就形成了高阶函数。
高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。
3,惰性计算
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算
4,声明式(表达式)
声明式代码:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
5,纯函数
输入一定时,输出一定):输出不受外部环境影响,同时也不影响外部环境,无副作用
immutable
纯函数的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的,也就是说不能多次赋值。
6,尾递归和尾调用
尾调用是函数式编程中一个很重要的概念,当一个函数执行时的最后一个步骤是返回另一个函数的调用,这就叫做尾调用。
尾递归是指在函数最后调用函数自身
7,函数柯里化
- 将多参数的函数转换成单参数的形式
- 将一个低阶函数转化为高阶函数的过程
8,函数compose
将多个函数合并成一个函数,并且和顺序无关
9,Point free
函数无须提及将要操作的数据是什么样的
这种风格能够帮助减少不必要的命名,让代码保持简洁和通用。
三、函子的简单介绍
1,函子的概念
前面介绍了范畴,将两个范畴关联起来的数据类型成为函子
函子也是基本的运算单位和功能单位。
2,函子的数据结构
任何具有map方法的数据结构,都可以当作函子的实现。
例子:
函子的of方法
函数式编程一般约定,函子有一个of方法,用来生成新的容器。
3,前端中函子的应用举例
例子
深入访问object的属性的时候,不会担心由于属性不存在、undefined、null等问题出现异常。
四、函数式编程的优势
1. 代码简洁,开发较快
函数式编程大量使用函数,功能抽离,减少了代码的重复,代码复用性高,因此程序比较短,开发速度较快。
2. 易于理解,接近自然语言
用描述性的表达式组合不同的函数形成程序,易于理解
比如:(1 + 2) * 3 - 4 <-------> subtract(multiply(add(1,2), 3), 4)
3. 易于代码的维护和管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
4. 易于"并发编程"
函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)
5. 易于代码升级和扩展
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。
五、函数式编程在前端的应用
1,框架层面的应用
1.1 React函数式编程
函数式编程时react框架设计的核心思想之一,每个组件其实都是一个函数,对这些函数进行组合之后形成data和view之间的映射关系,React自身可以看作是一个ui函数,入参是data
例子:
dom函数化(jsx),react算法中的尾递归,尾调用,fiber生成算法,hooks等
1.2 vue3函数式编程
vue3新增了一个新的函数setup,类似于react的hooks
1.3 Redux函数式编程
redux是函数式编程在解决前端数据流中很典型的应用。
中间件机制(compose),纯函数Reducer,createStore(enhancer)等
1.4 Rxjs函数式编程
Rxjs是一个解决复杂异步操作的库,结合了响应式编程和函数式编程。
1.5 koa中的函数式编程
中间件的实现compose
1.6 lodash中的函数式编程
lodash中的fp模块
还有很多框架有函数式编程的应用,这里不一一列举。
2,在单元测试中的应用
严格函数式编程的每一个符号都是对直接量或者表达式结果的引用,没有函数产生副作用。对被测试程序中的每个函数,只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态。所有要做的就是传递代表了边际情况的参数。如果程序中的每个函数都通过了单元测试,软件系统的质量就有了保证
3,在es6之后的应用
- 箭头函数
- 数组的扩展 find findIndex
- Map数据结构
- Reflect映射
- 对象的链判断运算符(MayBe函子) message?.body?.user?.firstName
- 函数扩展 尾调用优化
- 最新提案:函数的部分执行
- gua管道运算符 >|
...
4,允许可控制的副作用共存
- 前端可能的副作用
- 更改全局状态
- 发送一个 http 请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM 查询
- 访问系统状态
- 在无法避免副作用的情况下,使用函子和单子来控制副作用,也包括错误的处理。
- 许多异步解决方案其实是函子的具体实现,比如Promise是一个Monad
总结
1,前端函数式编程的现状
- 函数式编程的核心是引用透明,结果可预测,数据不可变,在编程过程中带来了易于维护,并行编程的开发体验,具有很重要的意义。
- 函数式编程为组件的编写提供了更高的灵活度与可读性,并且更符合一个前端编写者的习惯,目前在前端应用非常广泛。尤其以react, redux应用最为流行和广泛,以及其他衍生生态。
- JavaScript,作为最流行的编程语言之一,特别适合函数式编程
2,函数式编程趋势和未来展望
- cpu和内存性能的提高
- 函数式编程正逐渐成为计算机科学和软件工程领域的流行趋势
- 函数式编程发展的应该是思想,而不是以函数式编程为基础语言的发展
书籍推荐
参考: