什么是函数式编程?

什么是函数是编程?

我们通常很熟悉过程式编程, 面向对象编程, 那么函数式编程是什么呢? 函数式编程基于一个前提: 使用纯函数来构造程序 (即 函数没有副作用); 那么普通函数有哪些副作用呢? 比如执行I/O, 处理错误, 修改数据等等

一段带有副作用的程序

一个购买咖啡的例子

class Cafe {
  def buyCoffee(cc: CreditCard): Coffee = {
    var cup = new Coffee()
    cc.charge(cup.price)
    // 函数式语言不需要显式 return
    cup
  }
}

在上面这程序中 cc.charge(cup.price) 是有副作用的, 因为会调用 web service 联系信用卡公司扣费等, 副作用使得这段代码很难测试; 缺乏可测试性说明代码设计是有问题的, 我们可以让 buyCoffee 忽略掉扣费这件事情, 传入一个支付对象:

class Cafe {
  def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
    var cup = new Coffee()
    p.charge(cc, cup.price)
    cup
  }
}

以上把 Payments 对象单独独立出来, 增加了一些可测试性, 但如果需要购买多杯咖啡呢? 总不能循环 N 次吧? 让我们来看看函数式的解法:

class Cafe {
  def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
    var cup = new Coffee()
    (cup, Charge(cc, cup.price))
  }
  def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
    var purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
    var (coffees, charges) = purchases.unzip
    (coffees, charges.reduce((c1, c2) => c1.combine(c2)))
  }
}


case class Charge(cc: CreditCard, amount: Double) {
  def combine(other: Charge): Charge = {
    if (cc == other.cc) {
      Charge(cc, amount + other.amount)
    } else {
      throw new Exception("付费失败")
    }
  }
}

总体来看, 测试性已经增强了不少, 我们把 Payment 和 Cafe 出来, Cafe 完全忽略计算是如何处理的;

纯函数是什么

上面有说过, 函数式编程意味着使用纯函数, 纯函数是没有副作用的, 函数式编程另外一个好处: 更容易推理;

对于一个输入类型为 A , 输出类型为 B 的函数f A => B, 任何内部或外部过程的状态都不会影响到 f(a)的执行结果;
我们可以使用引用透明的概念对纯函数形式化, 当调用一个函数所传入的参数是引用透明的, 并且函数调用也是引用透明的, 那么这个函数就是纯函数

引用透明, 纯粹度以及替代模型

引用透明定义:

对于程序 p, 如果它包含的表达式 e 满足引用透明, 所有的 e 都可以替换为它的运算结果而不会改变程序 p 的含义;
假设存在一个函数 f, 若表达式 f(x) 对所有引用透明的表达式 x 也是引用透明的, 那么这个 f 是个纯函数;

class Cafe {
  def buyCoffee(cc: CreditCard): Coffee = {
    var cup = new Coffee()
    cc.charge(cup.price)
    cup
  }
}

在上面的 cc.charge(cup.price) 返回什么类型, 他都会被 buyCoffee 丢弃;

因此 buyCoffee 的运算结果只是一杯咖啡, 这相当于 new Coffee(), 根据我们定义的引用透明, 如果 buyCoffee 要满足纯函数. 对于任何 p 来讲, p(buyCoffee()) 的行为需要与 p(new Coffee) 相同, 这显然不成立; 表达式 new Coffee() 不作任何事, 而 buyCoffee 将会连接信用卡公司并授权计费, 两个程序显然有差异;

引用透明要求函数不论进行了任何操作都可以用它的返回值来代替, 这种限制使得推导一个函数的求值变得简单而自然, 我们称之为代替模型( substitution model); 如果表达式是引用透明的, 那么计算过程就像是数学方程, 一个又一个等价值所替代的过程, 换句话说 引用透明使得程序具备了等式推理(equational reasoning)的能力;