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除了返回一杯咖啡,还进行了交易过程。

引用透明的限制使得推导一个程序的求值变得简单,称为替代模型。如果表达式是引用透明的,可以想象计算过程就像是在解代数方程。展开表达式的每一部分,使用指示对象代替变量,然后归约到最简单的形式。在这一过程中,每一项都可以被等价的值替代,计算的过程就像是被一个又一个等价的值替代的过程。引用透明使得程序具备了等式推理的能力。