CH01-定义
介绍
函数式编程,只是用纯函数来构造程序,即函数是没有副作用的。
函数的副作用大致包括:
- 修改一个变量
- 直接修改数据结构
- 设置一个对象的成员
- 抛出一个异常或以一个错误停止
- 打印到终端或者接收用户的输入
- 读取或写入一个文件
- 在屏幕上绘图
- …
函数式编程的益处
副作用函数的实例
class Cafe{
def buyCoffee(cc:CreditCard):Coffee = {
val cup = new Coffee()
cc.charge(cup.price) // 副作用,对信用卡进行扣费
cup
}
}
扣费过程涉及与整个交易系统的交互,而这个方法的主要用途是为了得到一杯咖啡,因此,扣费动作在获取咖啡的过程中额外完成了,即副作用。
但是如果要测试这段程序,我们并不真的需要与交易系统进行交互,或者将交易信息进行持久。同时信用卡本身也不应该知道交易的细节,我们可以传递一个Payments
对象给buyCoffe
方法,让CreditCard
忽略掉交易的细节,整个交易过程由Payments
对象来完成:
class Cafe {
def buyCoffe(cc:CreditCard, p: Payments) = {
val cup = new Coffe()
p.charge(cc, cup.price) // 副作用,有交易对象来完成扣费
cup
}
}
现在可以使用 mock 来对交易细节进行测试。但是真个程序很难被复用,比如我们要订购 12 杯咖啡。
去除副作用
在上面的例子中,我们可以在购买咖啡的过程中同时返回咖啡和对应的费用,而根据费用进行的交易过程则后续进行处理,这样,这个程序就不再存在副作用:
def buyCoffe(cc:CreditCard):(Coffe, Change) = {
val cup = new Cup()
(cup, Charge(cc, cup.price))
}
这样通过将费用的创建与执行分离,购买咖啡的过程就不再有副作用,而真正的扣费可以在后续的事务中进行。
现在我们对相同信用卡的计费进行合并,以便支持同时购买多杯咖啡:
case class Charge(cc:CreditCard, amount: Double){
def combine(ohter:Charge): Charge = {
if (cc == other.cc) Charge(cc, amount + other.amount)
else throw new Exception("Can't combine charges to different cards.")
}
}
然后实现buyCoffes
来购买多杯咖啡,一次返回对应数量的咖啡和一个合并后的计费值:
def buyCoffes(cc:CreditCard, n:Int): (List[Coffe], Charge) = {
val purchases:List[(Coffe, Charge)] = List.fill(n)(buyCoffe(cc))
val (coffes, charges) = purchases.unzip
(coffes, chargs.reduce((c1,c2) => c1.combine(c2)))
}
如果需要将不同信用卡的不同消费记录合并成一个计费列表,即将相同的信用卡计费合并:
def coalesce(charges:List[Charge]):List[Charge] =
charges.groupBy(_.cc).values.map(_.reduce(_ combine _)).toList
如何消除副作用?
通过把副作用推到程序的最外层,来转换带有副作用的函数。对于很多必须的副作用都存在对应的函数式实现,如果没有对应的实现,也可以通过找到一种方式来构造代码,让副作用发生但不可见。
什么是纯函数
纯函数是没有副作用的,更易推理。
一个函数在执行过程中,除了根据给定的参数给出运算结果之外没有任何其他影响,即为无副作用。
引用透明:对于一个函数,无论何时调用,相同的参数都会返回一致的结果。同样也适用于表达式。
任何程序中符合引用透明的表达式都可以由它的结果替代,而不会改变程序的含义。
纯函数:当调用一个函数时,传入的参数是引用透明的,并且函数调用也是引用透明的,那么他就是一个纯函数。
引用透明与纯粹度
对于程序 p,如果它包含的表达式 e 满足引用透明,所有的 e 都能替换为它的计算结果而不会改变程序 p 的含义。假设存在一个函数 f,若表达式 f(x) 对所有引用透明的表达式 x 也是引用透明的,那么 f 是一个纯函数。
引用透明、纯粹度及替代模型
引用透明要求函数不论进行了何种操作都可以用它的返回值来代替。
比如一开始的咖啡例子:
def buyCoffee(cc:CreditCard):Coffee = {
val cup = new Coffee()
cc.charge(cup.price) // 副作用,对信用卡进行扣费
cup
}
它的返回值为cup
,实际上就是new Coffe()
。对于任何一个函数p(buyCoffee(cc:CreditCard))
与p(new Coffe())
显然不能进行替换,因为buyCoffee
除了返回一杯咖啡,还进行了交易过程。
引用透明的限制使得推导一个程序的求值变得简单,称为替代模型。如果表达式是引用透明的,可以想象计算过程就像是在解代数方程。展开表达式的每一部分,使用指示对象代替变量,然后归约到最简单的形式。在这一过程中,每一项都可以被等价的值替代,计算的过程就像是被一个又一个等价的值替代的过程。引用透明使得程序具备了等式推理的能力。
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.