1 函数式编程

函数式编程主要可以为当前面临的三大挑战提供解决方案:

  • 满足并发的普遍需求。有了并发,我们可以对应用进行水平扩展,并提供其对抗服务器故障的能力。
  • 满足编写数据导向程序的要求。有效处理海量数据的技术。
  • 满足编写无bug程序的要求。函数式编程从数学角度为我们提供了新的工具,使我们想无bug的程序又迈进了一步。

使用不可变值、被视为一等公民的函数、无副作用的函数、高阶函数以及函数集合,有助于编写出简洁、强大而又正确的代码。

1.1 纯函数

在函数内部,没有任何全局状态被修改的函数,称为无副作用函数,即纯函数。 纯函数极大地简化了函数的分析、测试和调试。你可以不考虑调用该函数的上下文信息,否则的话,就要受该上下文中调用的任何函数的影响。

纯函数带来了两点好处:

  • 你可以在任何地方调用函数,并确信其行为与上下文无关,每次的行为都能够确保相同。由于没有任何全局对象被修改,对函数的并发调用也是安全可靠的,不需要任何线程安全的编写技巧。
  • 可以用表达式所计算出的值替换表达式本身。

当一个函数采用其他函数作为变量或返回值时,它被称为高阶函数。在数学中,微积分中有两个高阶函数的例子——微分和积分。我们将一个表达式作为函数传给“微分函数”,然后微分函数返回一个新函数,即原函数的导函数。

1.2 不可变变量

在函数式编程中,变量是不可变的。这是数学原理带来的一个结果。

而一般的,使用变量可以保存对象的状态并进行修改,但不可变量不意味着函数式编程完全没有状态。我们可以用新的对象或新开的栈空间来表示状态的改变,即调用函数,并返回你期望的值。


In [1]:
def factorial(i: Int): Long = {
    def fact(i: Int, accumulator: Int): Long = {
        if (i <= 1) accumulator
        else fact(i - 1, i * accumulator)
    }
    fact(i, 1)
} 

(0 to 5) foreach ( i => println(factorial(i)) )


1
1
2
6
24
120
defined function factorial

我们用递归计算阶乘,对计算结果的每次更新被压进了栈上,而不是直接修改栈中的值。

值不可变性带来的好处:

  • 对编写可扩展的并发程序有巨大好处。引用透明的函数和值不可变性的结合,使得函数式编程成为了编写并发软件应用的更好选择。
  • 致命的bug大多来源于可变状态,尤其是那种部署到生产环境之前很难测试出来的bug,纯函数与值不可变性极大地降低了bug出现的概率。
  • 有了值的不可变性,我们可以将对象内部的内部数据设为可对外公开访问,而不必封装在对象中再提供特定的访问方法而担心数据的值被修改的问题。
  • 函数式编程通过共享对象中的未修改部分使得复制的开销最小化,这样不可变性的程序反而比状态可变的程序更快。

2 匿名函数、Lambda和闭包

  • 函数:一种具有名或匿名的操作。其代码直接被调用时才执行。在函数的定义中,可能有也可能没有引用外部的未绑定的变量
  • lambda:一种匿名函数。在她的定义中,可能有也可能没有引用外部的未绑定变量
  • 闭包:在定义中包含了自由变量,函数中包含了环境信息,以绑定其引用的自由变量

In [2]:
var factor = 2
val multiplier = (i: Int) => i * factor


factor: Int = 2
multiplier: Int => Int = <function1>

In [3]:
(1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _)


res2: Int = 122880

In [4]:
factor = 3
(1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _)


res3_1: Int = 933120

解释一下上面的代码:

  • factor变量是一个累乘因子,它决定了multiplier的行为
  • multiplier是一个不可变的函数字面量,其行为随着factor改变而改变
  • factor是一个自由变量,是当前作用域中某个值的引用。编译器创建了一个闭包,用于包含multiplier与它引用的外部变量的上下文信息,从而绑定了外部变量本身
  • multiplier引用了factor,每次调用时都重新读取factor的值。如果函数没有外部引用,那它就只是包含了自身,不需要外部上下文信息
  • 如果将multiplier传递给其他作用域(如另一个方法)中,自由变量factor的有效性会一直伴随着multiplier函数

3 递归与尾递归优化

在函数式编程中,递归是实现“循环”的唯一方法,因为你无法修改循环变量。

阶乘就是一个很好的例子。递归是表达函数的最常见方式。然而,它有两个缺点:仿佛调用函数带来了开销;栈溢出的风险

3.1 尾递归

编译器可以将尾递归优化成循环,避免了一般递归函数带来的风险。在尾递归中,函数可以调用自身,并且该调用时函数的最后一个操作。尾递归能把函数优化为循环,可以消除潜在的栈溢出风险,同时降低函数调用开销而提升效率。Scala使用@tailrec注解对尾递归进行检查。


In [5]:
import scala.annotation.tailrec


import scala.annotation.tailrec

In [6]:
def factorial(i: BigInt): BigInt = {
    @tailrec
    def fact(i: BigInt, accumulator: BigInt): BigInt =
        if (i == 1) accumulator
        else fact(i-1, i*accumulator)
    
    fact(i, 1)
}


defined function factorial

从上面的代码中看出,定义一个嵌套的尾递归函数,将累积值作为参数,是将很多普通递归算法转为尾递归的实用技巧。


In [7]:
for (i <- 1 to 10)
    println(s"$i:\t${factorial(i)}")


1:	1
2:	2
3:	6
4:	24
5:	120
6:	720
7:	5040
8:	40320
9:	362880
10:	3628800


In [8]:
// 调用大数10000,尾递归优化版本可以成功运行,而递归版本会在普通电脑上出现栈溢出
factorial(10000)


res7: BigInt = 284625968091705451890641321211986889014805140170279923079417999427441134000376444377299078675778477581588406214231752883004233994015351873905242116138271617481982419982759241828925978789812425312059465996259867065601615720360323979263287367170557419759620994797203461536981198970926112775004841988454104755446424421365733030767036288258035489674611170973695786036701910715127305872810411586405612811653853259684258259955846881464304255898366493170592517172042765974074461334000541940524623034368691540594040662278282483715120383221786446271838229238996389928272218797024593876938030946273322925705554596900278752822425443480211275590191694254290289169072190970836905398737474524833728995218023632827412170402680867692104515558405671725553720158521328290342799898184493136106403814893044996215999993596708929801903369984844046654192362584249471631789611920412331082686510713545168455409360330096072103469443779823494307806260694223026818852275920570292308431261884976065607425862794488271559568315334405344254466484168945804257094616736131876052349822863264529215294234798706033442907371586884991789325806914831688542519560061723726363239744207869246429560123062887201226529529640915083013366309827338063539729015065818225742954758943997651138655412081257886837042392087644847615690012648892715907063064096616280387840444851916437908071861123706221334154150659918438759610239267132765469861636577066264386380298480519527695361952592409309086144719073907685857559347869817207343720931048254756285677776...

3.2 尾递归的trampoline优化

考虑一种递归情况:函数A不调用自身,而是调用另一个函数B;而函数B又调用A,如此反复循环。trampoline可以将这种反复来回调用的函数转化为循环。

所以,trampoline是一种无需递归就可以处理来回调用的计算的数据结构。Scala库中有尾递归对象来达到这种目的。


In [9]:
import scala.util.control.TailCalls._


import scala.util.control.TailCalls._

In [10]:
def isEven(xs: List[Int]): TailRec[Boolean] =
  if (xs.isEmpty) done(true) else tailcall(isOdd(xs.tail))

def isOdd(xs: List[Int]): TailRec[Boolean] =
 if (xs.isEmpty) done(false) else tailcall(isEven(xs.tail))


defined function isEven
defined function isOdd

上面的确定一个数是否是偶数的方法是使用isEven和isOdd相互引用,效率不高,其中代码对列表中元素进行来回调用,如果到列表结束时,处于isEven方法中就返回true,否则返回false。

其中TailCalls包中,done方法返回递归调用的最后结果,而tailcall执行递归调用。


In [11]:
for (i <- 1 to 5) {
    val even = isEven((1 to i).toList).result
    println(s"$i is even? $even")
}


1 is even? false
2 is even? true
3 is even? false
4 is even? true
5 is even? false

4 偏应用函数与偏函数

  • 偏应用函数是带部分而非全部参数列表的函数,其返回值是一个新的函数,新函数负责携带剩下的参数列表。
  • 偏函数是单参数函数,并未对该类型的所有值都有定义。偏函数的字面量语法由包围在花括号的一个或多个case语句构成。

In [12]:
// 偏应用函数的例子
def cat1(s1: String)(s2: String) = s1 + s2

val hello = cat1("Hello ") _
hello("World!")


defined function cat1
hello: String => String = <function1>
res11_2: String = "Hello World!"

In [13]:
cat1("Hello ")("World!")


res12: String = "Hello World!"

In [14]:
// 偏函数的例子
val inverse: PartialFunction[Double, Double] = {
    case d if d != 0.0 => 1.0 / d
}


inverse: PartialFunction[Double, Double] = <function1>

In [15]:
inverse(1.0)


res14: Double = 1.0

In [16]:
inverse(2.0)


res15: Double = 0.5

In [17]:
inverse(0.0)


scala.MatchError: 0.0 (of class java.lang.Double)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
	cmd13$$user$$anonfun$1.applyOrElse(Main.scala:89)
	cmd13$$user$$anonfun$1.applyOrElse(Main.scala:89)
	scala.runtime.AbstractPartialFunction$mcDD$sp.apply$mcDD$sp(AbstractPartialFunction.scala:36)
	cmd16$$user$$anonfun$1.apply$mcD$sp(Main.scala:97)

偏函数与返回Option函数之间的转化

如果我们有一个偏函数,同时又不希望发生抛出异常的情况,可以将偏函数提升为一个返回option的函数(使用lift进行转),同时也可以将返回Option的函数“降级”为偏函数。


In [18]:
val finicky: PartialFunction[String, String] = {
    case "finicky" => "FINICKY"
}


finicky: PartialFunction[String, String] = <function1>

In [19]:
finicky("finicky")


res18: String = "FINICKY"

In [20]:
finicky("other")


scala.MatchError: other (of class java.lang.String)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
	cmd17$$user$$anonfun$1.applyOrElse(Main.scala:95)
	cmd17$$user$$anonfun$1.applyOrElse(Main.scala:95)
	scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
	cmd19$$user$$anonfun$1.apply(Main.scala:101)
	cmd19$$user$$anonfun$1.apply(Main.scala:100)

In [21]:
val finickyOption = finicky.lift


finickyOption: String => Option[String] = <function1>

In [22]:
finickyOption("finicky")


res21: Option[String] = Some(FINICKY)

In [23]:
finickyOption("other")


res22: Option[String] = None

In [24]:
val finicky2 = Function.unlift(finickyOption )


finicky2: PartialFunction[String, String] = <function1>

In [25]:
finicky2("other")


scala.MatchError: other (of class java.lang.String)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
	scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
	cmd17$$user$$anonfun$1.applyOrElse(Main.scala:95)
	cmd17$$user$$anonfun$1.applyOrElse(Main.scala:95)
	scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
	cmd24$$user$$anonfun$1.apply(Main.scala:109)
	cmd24$$user$$anonfun$1.apply(Main.scala:108)

5 Curry化

Curry将一个带有多参数的函数转换为一系列函数,每个函数都只有一个参数。

5.1 定义curry化函数


In [26]:
// 定义curry化的函数
def cat2(s1: String) = (s2: String) => s1 + s2


defined function cat2

In [27]:
// 将curry化的函数作为偏应用函数时,不需要在后面加下划线
val cat2hello = cat2("Hello ")


cat2hello: String => String = <function1>

In [28]:
cat2hello("World!")


res27: String = "Hello World!"

我们可以将一个带有多个参数的方法转化为curry化的形式


In [29]:
def cat3(s1: String, s2: String) = s1 + s2


defined function cat3

In [30]:
val cat3Curried = (cat3 _).curried


cat3Curried: String => String => String = <function1>

In [31]:
cat3Curried("hello")("world")


res30: String = "helloworld"

下面我们用函数字面量来定义:


In [32]:
val f1: String => String => String = (s1: String) => (s2: String) => s1 + s2


f1: String => String => String = <function1>

In [33]:
val f2: String => (String => String) = (s1: String) => (s2: String) => s1 + s2


f2: String => String => String = <function1>

In [34]:
f1("hello")("world")


res33: String = "helloworld"

In [35]:
f2("hello")("world")


res34: String = "helloworld"

类型签名String => String => String和String => (String => String)是等价的。调用f1或f2时绑定第一个参数列表,将会返回一个类型为String => String的新函数。

5.2 去curry化

使用Function中的一个方法对函数去curry化


In [36]:
val cat3Uncurried = Function.uncurried(cat3Curried)


cat3Uncurried: (String, String) => String = <function2>

In [37]:
cat3Uncurried("hello", "world")


res36: String = "helloworld"

In [38]:
val ff1 = Function.uncurried(f1)


ff1: (String, String) => String = <function2>

In [39]:
ff1("hello", "world")


res38: String = "helloworld"

5.3 curry与偏应用函数

curry的一个实际用处是对特定类型的数据函数做特殊化。函数可以接受通用的类型,而curry化的函数形式则只接受特定的类型。

curry与偏应用函数式紧密相关的两个概念,这两个概念可以相互替换。

5.4 元组形式参数的转换


In [40]:
def mult(d1: Double, d2: Double, d3: Double) = d1 * d2 * d3


defined function mult

In [41]:
val d3 = (2.2, 3.3, 4.4)


d3: (Double, Double, Double) = (2.2, 3.3, 4.4)

In [42]:
mult(d3._1, d3._2, d3._3)


res41: Double = 31.944000000000003

如果可以将元组直接作为参数放入函数中就很相称且便利了,Function对象提供了元组形式和非元组形式的方法


In [43]:
val multTupled = Function.tupled(mult _)


multTupled: (Double, Double, Double) => Double = <function1>

In [44]:
multTupled(d3)


res43: Double = 31.944000000000003

In [45]:
val multUntupled = Function.untupled(multTupled )


multUntupled: (Double, Double, Double) => Double = <function3>

In [46]:
multUntupled(d3._1, d3._2, d3._3)


res45: Double = 31.944000000000003