今天要阅读的代码来自《Scala By Example》一书的第一个例子。这两段代码通过实现一个快速排序算法体现了命令式与函数式之间的区别。这种直观的对比无疑很好地展现了函数式编程的优雅与简洁。让我们来看看这两段代码,首先是命令式的实现方式:
def sort(xs: Array[Int]) {
def swap(i: Int, j: Int) {
val t = xs(i); xs(i) = xs(j); xs(j) = t
}
def sort1(l: Int, r: Int) {
val pivot = xs((l + r) / 2)
var i = l; var j = r
while (i <= j) {
while (xs(i) < pivot) i += 1
while (xs(j) > pivot) j -= 1
if (i <= j) {
swap(i, j)
i += 1
j -= 1
}
}
}
if (l < j) sort1(l, j)
if (j < r) sort1(i, r)
sort1(0, xs.length - 1)
}
|
下面是函数式的方式:
def sort(xs: Array[Int]): Array[Int] = {
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
}
|
后者的简洁不言而喻,感觉实在太强烈了。它还展露出一种优雅的从容,因为没有嵌套的while循环,清扫了许多阅读障碍,没有繁文缛节,直指问题本质,显得游刃有余,挥洒自如。究其根由,在于这种函数式的编程方式,完全匹配快速排序的算法原则与过程,就像是那种斩钉截铁的证明,没有多余的啰嗦,结果如同“清水出芙蓉,天然来雕饰”。
不提这种感觉的美感,函数式编程带来的实实在在好处在于它的无副作用特质。这就好似你寻找的药方,不仅能够药到病除,服用后还没有不良反应,真可以说得上奢望了。阅读第二段代码,我们可以非常直观地看到没有任何操作修改了传入的xs数组。从外向内看,返回的数组是通过Array.concat将三段数组给串联了起来,返回了一个新的数组对象。表面看来,这段代码对xs做了filter操作,根据传入的Predicate对数组元素进行筛选。事实上,filter同样是函数,它并没有直接更改被操作的数组,而是返回了一个新的筛选后的数组对象。这意味着,即使我们传入一个val的数组对象,这个sort函数也是不会抱怨的。例如:
val target = Array(1, 2, 5, 3, 8, 4, 7)
val result = sort(target)
|
这两种方案皆使用了递归,时间复杂度皆为O(N log(N)),但就简洁性和易读性而言,却不可同日而语。而这种无副作用特性则体现了函数式的不变特质,从而可以极大地简化并发编程模型。当然,这种方法必然会造成空间的浪费;不过,有JVM提供的GC负责内存管理,我们也无需关心这些对象在何时需要被释放。只要系统对内存的要求没有特别的限制,这一问题几乎可以忽略不计。
好吧,让我们再转到Scala语言层面的特性上来。看第一段代码,除了个别关键字与语法不同之外,它几乎与Java代码没有太大的区别,最大的不同还在于Scala将函数(或者说方法)提升到了一等公民。第二段代码中,比较特殊的用法是调用Array的filter函数。该函数的签名为:
def filter(p: T => Boolean): Array[T]
|
调用时,这段代码传入的表达式比较奇怪。严格意义上,filter显然需要传入一个函数,这个函数要求一个输入参数,返回为Boolean型。如果采用匿名函数的方式,调用方式应该为:
xs.filter(x => pivot > x)
|
如果使用变量的placeholder,则可以表示为:
xs.filter(_ => pivot > _)
|
而这里使用的则是一种称为partially applied function的方式,它支持我们在不会引起歧义的情况下(主要是指只有一个参数的情形),直接省略该参数变量。只要明白这种语法,这样的代码仍然是可读的。