函数式编程近年来越来越火。函数式编程的语言鄙视没有函数式编程的语言,纯函数式编程的语言鄙视函数式编程不纯的语言。
那么,到底什么是函数式编程,函数式编程的核心思想是什么?
函数式编程的第一个特点是一个函数可以作为参数传递给另一个函数,这个函数叫做高阶函数。例如,要对数组进行排序,可以传入一个排序函数作为参数:
String[]数组={ 'orange ',' Pear ',' Apple ' };
Arrays.sort(array,string : compare toignorecase);函数式编程的第二个特点是可以返回一个函数,这样就可以实现闭包或者惰性计算:
以上两个特性只是简化了代码。从代码可维护性的角度来看,函数式编程最大的优势是透明引用,即函数运算的结果只取决于输入参数,而不取决于外部状态。因此,我们常说函数式编程没有副作用。
没有副作用有一个很大的好处,就是函数是内部无状态的,也就是输入是确定的,输出是确定的,很容易测试和维护。
很多初学者容易纠结于“纯”函数式语言,认为只有去除变量和副作用的Haskell才是正宗的函数式编程。甚至有人认为纯函数不能有任何IO操作,包括日志记录。
其实这种纠缠是没有意义的,因为计算机的底层是一个完全可变的内存和不可预测的输入系统,追求完美和没有副作用是不现实的。我们只需要理解函数式编程的思想,让业务逻辑“没有副作用”。至于变量、日志、读缓存等无关紧要的“副作用”,不用担心,不用解决,几乎不可能解决。
让我们举个栗子。
比如一个金融软件需要一个计算个人所得税的功能。输入是收入记录,输出是税额:
double calculateIncomeTax(收入记录){ 0
.
}假设IncomeRecord又是这样的:
类别输入记录{
字符串id;//身份证号
字符串名称;//名称
双倍工资;//工资
暂时不考虑五险一金,我们只关注如何算税。为简单起见,我们假设在直接扣除免税金额后,按20%计算:
double calculateIncomeTax(收入记录){ 0
双阈值=3500;
双重税=记录。工资=起征点?0 :(记录.工资-阈值)* 0.2;
报税;
}以上手续在2018年9月1日前没有问题。问题是2018年9月1日之后,门槛调整为5000,所以2018年8月和2018年9月的计算结果应该不一样。怎么改?
普通开发商的改革:不是很简单吗?只需直接获取当前日期并返回正确的阈值:
double calculateIncomeTax(收入记录){ 0
双阈值=今日()日期(2018年9月1日)?3500 : 5000;
双重税=记录。工资=起征点?0 :(记录.工资-阈值)* 0.2;
报税;
}程序是对的,但问题是:
同样的输入,8月31日运行,9月1日运行,结果不同。会计在9月1日做8月工资表之前要不要把电脑时间调整到8月?
从函数式编程的角度想想,就会发现问题:
Today()是一个返回与时间相关的结果的函数,它导致calculateIncomeTax()与当前时间相关,而不是一个纯函数。
那么如何将calculateIncomeTax()恢复为纯函数,同时支持阈值的调整呢?
方法是传入与时间相关的变量作为参数,例如
,给IncomeRecord增加几个字段:class IncomeRecord { String id; // 身份证号 String name; // 姓名 double salary; // 工资 int year; // 年 int month; // 月 }这样我们就可以消除today()的调用:
double calculateIncomeTax(IncomeRecord record) { double threshold = date(record.year, record.month) < date(2018, 9) ? 3500 : 5000; double tax = record.salary <= threshold ? 0 : (record.salary - threshold) * 0.2; return tax; }calculateIncomeTax()又变成了一个纯函数,会计就不用改电脑时间了。
是不是觉得这个例子太简单了?其实简单的函数如果都能写成有状态的,那么复杂的业务逻辑必然写成一锅粥。
举个复杂的栗子:
对于一个股票交易系统,如果我们把输入定义为:开盘前所有股民的现金和持股,以及交易时段的所有订单,那么,输出就是收盘后所有股民的现金和持股:
StockStatus process(StockStatus old, List<Order> orders) { ... for (Order order : orders) { ... sendExchangeResult(...); // 给每一笔成交发送信息 } ... }很显然这是一个纯函数,虽然在处理过程中,这个函数会给股民朋友发送各种心跳消息。
如果把交易系统的模型设计成这样一个纯函数,那么理论上我们只需要从股市开市的那一天开始,把所有订单全部处理一遍,就可以正确得到今天收盘后的状态。
或者说,只要取任意一天开盘前的系统状态的备份(就是整个数据库的备份),把当天的订单重新处理一遍,就得到了当天收盘的状态。这个过程可以做任意次,结果不变,因此,非常适合验证代码的修改是否影响了业务流程。
那么问题来了,交易系统中有无数和时间相关的状态,怎么处理成纯函数?这个模型的处理,可比计算个税复杂多了。
这就是函数式编程的精髓:业务系统模型无状态。模型的好坏,直接影响到代码的正确性、可靠性、稳定性,以及是否需要996。