函数式编程的一些心得与体会
本篇内容主要分为这三个部分。
1.什么是函数式编程 2.如何编写 3.优缺点
什么是函数式编程
首先,到底什么是函数式编程呢?函数式编程跟命令式编程一样,是一种编程范式。命令式关注的是解决问题的步骤,函数式编程关注的内容则是数据之间的映射关系。
举个例子,假如你现在去 Google 面试,面试官让你把二叉树翻转。几乎不假思索,(你啪的一下,很快啊)就写了出来。
typescript
好,我们可以看看这个代码究竟代表着什么?其含义很明确,先判断节点是否为空,然后翻转左子树,然后是右子树,最后左右交换。
这就是命令式编程,你要完成什么事情,就要把能完成这个事情的步骤一一列举,然后让机器去运行这些步骤。
实际上,这也是命令式编程的理论模型——图灵机的特点。一条写满数据的纸带,一个根据纸带上的内容进行运动的机器。 这时候,如何使用函数式编程来实现呢?翻转二叉树本质上是这么一种映射:
F(Tree(left, right)) = Tree(F(right), F(left)); 根据这个思路,我们可以写出这样的代码。
typescript
这段代码同样也能达到翻转二叉树的效果,然而却体现了一种跟命令式不同的思维模式——通过描述“旧树 - 新树”之间的映射。函数式的代码这种“对映射的描述"肯定不仅仅是可以用于表达二叉树。也可以表达各种计算机中的数据结构中的映射。那么该如何进行函数式编程呢?
如何实践
在函数式编程里,以下这三点具有代表性思路。
1.纯函数 2.管理副作用 3.数据不变性
纯函数
首先是纯函数,它基本上都会第一时间出现在各种函数式编程的教材。纯函数的性质只有两个:
1.输出取决于输入参数 2.不产生副作用
多说无谓,举一个简单的例子,假设你在2017年8月份前现在接到了一个财会软件的需求,要求你根据某位员工的工资,计算他的个人所得税。忽略繁杂的五险一金的计算,你可以得到这样一个简单的函数。
typescript
代码中,calculateIncomeTax 这就是一个纯函数,输出只依赖于输入参数(工资)无论你是 5k 还是10k,税收都是从 3.5k 开始计算,只要你的收入一直是这个数,那么这个个税就是不会变的。
管理副作用
接着看,到了2018年8月,你接到通知,工资的起征点从9月份开始有了新的变化,从 3500 变成 5000 。需求很简单,你用键盘飞快地敲出了这样的代码。
typescript
仔细思考一下上面的代码就会发现,上面的代码无法处理边界问题,如果会计在9月份结算8月份的工资,难不成还要修改电脑日期吗?这很明显是一个bug,主要原因是由于引入了包含副作用的 I/O 函数(today),所以会导致输出不再依赖于输入参数,反而依赖于外部时钟,以至于原本的函数脱离了纯函数的范畴。
在这里,需要给出副作用的定义。那么什么是副作用呢?
1.执行 I/O 操作 2.改变输入参数 3.抛出异常 4.全局状态(指函数作用域外的状态)突变
刚刚的问题对应于这里的第一种情况。由于纯函数的输出只能依赖于输入参数,所以,转换一下思路,把产生副作用的内容当作参数传入,这样就让纯函数保持纯洁。today 的本质是通过系统的时钟来获取,那么它的每次调用就会导致全局状态突变。回到问题,其实只要我们把年月作为参数传入即可。
typescript
通过上述函数,无论我在什么时候特点的时间进行运算,只需要带上这位员工工资所在月份和年份,那么即可得到正确的结果。
数据不变性
数据不变性(Immutability)是函数式编程的基石之一,它要求“数据一旦创建就不可被修改”。
假设你在做一个电商后台,需要给订单重新计算总价并清理已下架的商品行:
typescript
这里的问题对应于产生副作用的第二种情况:改变输入参数。
在 recomputeTotal 函数里,我们直接把传进来的 order 对象就地修改:先 splice 掉数量为 0 的订单行,再把计算出的总价写回 order.amount。
这样一来,调用者手里的那份原始数据被悄悄篡改,违背了“数据一旦创建就不可变”的准则;后续任何依赖旧订单的逻辑都会因此产生难以追踪的 Bug,也无法放心地做并发或回溯。
要解决以上问题,只需要计算后返回一个新的订单,可轻松避免这种副作用。
typescript
优缺点
最后,总结一下内容函数式编程的优缺点,函数式编程的好处:
- 更加简洁、易读、易于测试的代码。
- 更好的支持并发。
但是,函数式编程也存在一些不可调和的问题:
- 难以调试的代码(调用栈过长,找不到目标代码,并且容易爆栈)。
- 大部分语言都难以支持一些高级类型(并、或),或类型推倒。
- 无法严格避免共享状态可变。
那么,在认识到这些问题的情况下,函数式编程与业务结合方面,让业务逻辑变成纯函数,变得可预测。而你的数据、和与外部系统的交互这些不可预测的内容,则通过“函数式核心-命令式外壳”的架构模式,把它们隔离在系统的边缘。具体落地时,可以把整个系统想象成一颗洋葱。
最后
文中部分内容引用自以下文章和书籍。
- [1] 什么是函数式思维 - nameoverflow[1]
- [2] 函数式编程的核心思想 - 廖雪峰[2]
- [3] JavaScript函数式编程 - Micheal Fogus
- [4] Haskell 趣学指南 - Miran Lipovaca
References
[2] 函数式编程的核心思想 - 廖雪峰