90%的人只是在用scala语法写程序,并不是函数式编程
点击蓝字
作者 | 小猴
编辑 | 小猴
分享Java、大数据内容
# 本篇要解决的问题
是不是我们用了scala或者Java的stream/Lambda表达式就是函数式编程了?
什么是FP?
函数的副作用,也就是side effect是什么意思?
请说一下pure function的好处?
函数式编程中的pure function提倡不修改变量、不连接外部系统,那咋写程序?
什么是RT(引用透明)?
什么是FP中的替换原则?
随着Spark、Flink的流行,scala慢慢越来越多地进入人们的视野。说到scala,大家都会想到函数式编程。但其实在绝大多数项目中,都是混合编程的style。也就是把面向对象和函数式编程掺杂在一起。这并没有什么问题,因为如果项目到达一定的工程规模,OO是必须要考虑的。但FP是什么,其实很多人并不清楚,大多数人通过链式编写一些数据处理,例如:list.filter.map.flatMap.redue,就以为已经掌握了函数式编程。然而,并不是。
1
到底什么才是FP(函数式编程)?
说到FP,大家都会提到基于stream的流式编程。但真的就是流式编程吗?那为什么不叫Streaming Programming,而叫Functional Programming呢?这其中必有蹊跷。函数式编程中最重点就是函数。从我们最早学习的C语言中,其实就有了函数的概念,我们编写的程序都是用函数、各种全局变量、宏等组成的。但大家没,没有人把C称之为函数式编程语言。
要理解FP,首先得理解这里的函数跟以往我们说认知的函数是不一样的。这里的函数是没有副作用的函数,很多时候,我们把它称之为Pure Function,也就是所谓的纯函数。在一个函数中,如果在一个函数中,修改了某个变量、修改了数据结构、在对象上设置新的字段、出现异常导致程序终止、从标准输入读取或者打印数据到控制台,再或者是操作IO读取或者写入文件。这样的函数,我们把他成为是有side effect的函数,也就是有副作用的函数。而如果一个函数仅仅是进行数据计算,简单地返回结果,那么它就是pure function。
Pure Function是有很多好处的,因为它强调的是Unmodified,而且和外部的数据是没有依赖的,所以Pure Function容易测试、也容易被重用、并行化处理,也不容易出错。
看到这里,相信大家会比较吃惊,FP是要基于Pure Function编程?但Pure Function连变量都不然修改、赋值、不然访问IO,要求如此苛刻,从未听闻。这根本没法编程啊。难以想象,在那跟我扯呢吧?
接下来,我们就来看看我们应该如何基于FP来设计我们的应用。
2
一段有side effect的代码
// 信用卡
class CreditCard {
def charge(money: BigDecimal) = {}
}
// 商品trait
trait Goods
// IPhone12商品
case class IPhone12(price:BigDecimal) extends Goods
// 商场
class SuperMarket {
def buyIphone12(cc: CreditCard): IPhone12 = {
// 创建一个ihpne12对象
val iphone12 = new IPhone12(12000)
// 向信用卡支付费用
cc.charge(iphone12.price)
iphone12
}
}
object SuperMarket {
def main(args: Array[String]): Unit = {
val market = new SuperMarket
val card = new CreditCard
market.buyIphone12(card)
}
}
简单解释下上面的代码,这个buyIphone12表示去商场买一个iphone12。然后,你递过去了你的信用卡。然后系统创建一个IPhone12对象,并且基于你的信用卡开始支付费用,然后把这个Iphone12手机打单,然后你就可以快乐地去拍照、炫耀了。
第4行代码,对外部传入的cc信用卡对象进行了外部操作,例如:支付需要和外部的信用卡公司连接,然后进行交易授权、收费,还要保存交易记录以便将来查询。而这个函数仅仅是需要一个新的IPhoen12对象,所以很明显这个函数是副作用的函数,是在其他的层面上产生了副作用。这里把它称之为side会更准确些,而这种副作用后续我们就称为side effect。
现在因为函数有了side effect,就会导致代码是比较难测试的,在测试这个函数期间,我们肯定是不希望它真的去连接信用卡公司进行交易。可重用性,也降低了很多。设想,如果我们是一个土豪,要给我们的女朋友们共买8个IPhone12手机,那我们需要调用这个buyIphone12 8次,然后连接信用卡公司12次….,你觉得商场会愿意吗?
大家对比一下上面的两种设计,一种是有Side Effect的,一种是没有Side Effect。没有side effect的设计,首先,买东西需要有信用卡,所以传入一个信用卡,但在buyIphone12中并没有直接连接信用卡服务器,而是把这部分工作交给了费用,并返回一个商品。现在buy就不再也side effect了,它只是简单地返回商品和费用。而如果要买多个手机,可以将多个费用合并成一个费用。同样,也是没有side effect的。
我们按照Pure Function的理念重新设计一个版本:
// 信用卡
class CreditCard
// 商品trait
trait Goods
// IPhone12商品
case class IPhone12(price:BigDecimal) extends Goods
// 费用对象
class Charge(val cc:CreditCard, val money:BigDecimal) {
// 合并多个费用
def combine(otherCharge:Charge) = {
if(cc.eq(otherCharge.cc)) new Charge(this.cc, this.money + otherCharge.money)
else throw new Exception("不是一张信用卡")
}
}
// 商场
class SuperMarket {
def buyIphone12(cc: CreditCard) = {
// 创建一个ihpne12对象
val iphone12 = new IPhone12(12000)
// 向信用卡支付费用
(iphone12, new Charge(cc, iphone12.price))
}
def buyIphone12(cc: CreditCard, n:Int):(List [IPhone12], Charge) = {
// 生成n个Iphone12以及n个费用对象
val iphone12AndCharges = List.fill(n)(buyIphone12(cc))
// 拉开列表
val (iphone12List, chargeList) = iphone12AndCharges.unzip
// 合并费用对象
(iphone12List, chargeList.reduce((charge1, charge2) => charge1.combine(charge2)))
}
}
object SuperMarket {
def main(args: Array[String]): Unit = {
val market = new SuperMarket
val card = new CreditCard
market.buyIphone12(card)
}
}
首先,定义了一个Charge对象,它用来表示费用。所以费用和信用卡对象已经分离了。
其次,为了能够处理一次买多个iphone的情况,在Charge中设计了combine函数,它可以用来将两个费用合并为一个(前提是一张信用卡)。
我们看到,现在的程序中的函数全部都是pure function,是没有side effect,它在我们编写程序的时候,就可以马上测试,而并不依赖外部环境,而且代码的复用性也会比较好。
要测试这两个函数非常容易,无需连接信用卡服务器就可以完成测试。而且代码是可扩展的,例如:用户要买一个电脑,也是一笔费用,费用依然可以被重用。
FP编程让我们重新思考设计,它和OOP的设计思路是不一样的。而持续遵循FP的方式对于我们每一位开发人员都意义重大,因为它会我们的程序的组织方式产生非常深远的影响。
但在于实际的应用中,我们肯定是需要访问外部系统的。那FP如何处理呢?例如:上述的Charge费用肯定是要连接信用卡服务器来进行交易的。我们可以先将费用的处理与费用的实际交易分离出来,然后将一些有side effect的操作尽量推到程序的外层。所以Function Programmer经常说,使用pure function作为内核,然后将一些产生effect的推到外部,并是一个很薄的层中处理。只要是在函数主体中创建的对象,并进行修改、外部连接,而对于FP的pure function内核感知不到。就很好了。
基于上述方式,我们可以将FP应用在基于Web Context的开发中、基于Spark、Flink的分布式引擎开发中。FP将无处不在,并且能够很好地和OOP结合在一起。
3
进一步解释Pure Function
前面我们给Pure Function的解释是纯函数,也就是这个函数没有任何的side effect,这样函数是容易被测试、扩展、重用的。
在FP中,我们所指的函数都是Pure Function。而向C、C++语言中编写的函数,其实严格来说,叫过程。以前我们写C程序的时候,根本没有考虑side effect的问题。所以C叫面向过程编程语言,是不能称之为面向函数编程语言的。因为对FP的支持,所以大家看到scala默认的collection是不可变的,从JDK9开始,Java也开始支持快速创建不可变的集合。
上图,说明了,f函数只是将类型的A的每个值a,转换为B类型的值b。内部或者外部的任何状态修改都与计算结果无关,整个计算是一个封闭的过程。如果f真的是一个Pure Function,那么它不会做任何额外的事情。
其实,我们在很早已经就开始接触Pure Function了。例如:对两个整数相加(+)
大家看上面的这张图,这不就是一个Pure Function吗?
4
RT(引用透明)和替换模型
RT表示引用透明。RT是一种属性,这种属性不仅仅针对函数,还针对一般的表达式。例如:2 + 3就是一个表达式,这个表达式是应用加法这个pure function来计算结果。没有任何的side effect。考虑下面的一行代码:
x - ( 2 + 3) % 2
如果,我们将2 + 3换成是5,对结果会有影响吗?会影响程序的行为吗?
答案肯定是不会的。这就是RT,它表示可以将表达式随便替换为表达式的结果。而针对pure function,当函数调用时,传递给函数的参数是RT的,这个函数就是RT的,这个函数就是pure function。
大家可能会觉得比较疑惑?“我怎么感觉所有的表达式、函数都是RT的”。来看看下面这段代码:
object RTDemo {
var a = 10
def main(args: Array[String]): Unit = {
val b = { a = a + 1
a
} + 10
if(a == 10) print("yup")
else print("no")
}
}
我们可以算出来,b经过计算后是21。那我们用21和表达式做一个替换,程序的结果还一样吗?
替换之前:输出no
替换之后:输出yup。
那你告诉我:
{ a = a + 1
a
} + 10
这个表达式是RT吗?显然不是。
同样,再看一下我们最初信用卡支付买手里的例子:
def buyIphone12(cc: CreditCard): IPhone12 = {
// 创建一个ihpne12对象
val iphone12 = new IPhone12(12000)
// 向信用卡支付费用
cc.charge(iphone12.price)
iphone12
}
函数返回值是new IPhone12(12000),我们能直接用一个new IPhone12(12000)替代buyIphone12函数的调用吗?显然是不可以的,因为如果我们以一个new的对象替代buyIphone12函数,那么支付费用的过程是不会执行的,相当于少算了一笔钱….你问问老板同意不?通过RT的定义,我们也可以很明显的看出来,上面的buyIphone12也不是RT的。
来给Pure function一个正式一点的定义:
如果对于一个表达式e它要是RT的,那么所有的程序中,只要表达式出现的地方,都可以用表达式的结果来进行替代,而且不会影响p的行为和意义,那么这个表达式就是RT的。如果如果表达式f(x)对于具备RT属性的x也是具备RT属性的,那么它就是RT的。
替换模型
我们要判断一个函数或者表达式是不是purity的,其实就用该函数或者表达式的结果进行一次替代就能有答案了。
参考文献:
https://www.manning.com/books/functional-programming-in-scala
# 总结
是不是我们用了scala或者Java的stream/Lambda表达式就是函数式编程了?
不是。函数式编程是一种思想,并不是链式API的使用。只有理解FP的思想,才能够进行函数式编程。
什么是FP?
FP是函数式编程,但这里所指的函数是纯函数,也就是pure function。也就是没有任何副作用的函数。
函数的副作用,也就是side effect是什么意思?
一个函数如果引用了外部变量并对其修改、或者连接外部系统等操作,就是side effect。也就是说,除了执行函数本身的计算结果之外,它还会会其他影响或者被影响的behavior。
请说一下pure function的好处?
pure function能够更好地进行测试,因为它没有外部系统的依赖。
pure function因为不和外部系统关联,容易被其他代码复用。
pure function的逻辑清晰,比较容易进行推理验证。
函数式编程中的pure function提倡不修改变量、不连接外部系统,那咋写程序?
核心的业务逻辑是基于pure function为核心来处理的,而连接外部系统放在最外的一层,薄薄得一层。
什么是RT(引用透明)?
RT表示一个表达式或者一个函数,可以被它们的计算结果直接替代,并且替代后并不会影响使用它们的程序的行为。
什么是FP中的替换原则?
就是把函数、或者表达式,直接用一个结果值进行替换。以此来验证它们是不是RT,验证函数是不是Pure function。