关于本系列
本系列文章旨在将您的思维方式向函数式思维方式调整,使您以全新的角度来思考常见问题,并提高您的日常编码工作。本系列介绍了函数式编程概念,函数式编程在Java 语言中运行的框架、在JVM 上运行的函数式编程语言、以及语言设计未来的一些方向。本系列主要面向了解Java 以及其抽象层的工作方式,但缺乏函数式语言使用经验的开发人员。
在本系列的第一期中,我首先讨论函数编程的一些特点,并演示如何在Java和其他函数语言中体现这些观念。在本文中,我将继续讨论这些概念,讲解一级函数、优化和闭包。但本期的内在主题是控制:什么时候想要控制、什么时候需要控制、什么时候应该放弃控制。
一级(First-class)函数和控制
我在上一部分最后通过使用Functional Java库(见参考资料),演示了用函数isFactor()
和factorsOf()
方法实现数字分类器,如清单1所示:
清单1. 数字分类器的函数版本
public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
public int sum(List<Integer> factors) {
return factors.foldLeft(fj.function.Integers.add, 0);
}
public boolean isPerfect(int number) {
return sum(factorsOf(number)) - number == number;
}
public boolean isAbundant(int number) {
return sum(factorsOf(number)) - number > number;
}
public boolean isDeficiend(int number) {
return sum(factorsOf(number)) - number < number;
}
}
在isFactor()
和factorsOf()
方法中,我停止了对框架循环算法的控制—它决定如何通过最好的方式遍历所有数字。如果框架(或者—如果您选择一门函数语言,如Clojure或Scala —语言)能优化底层实现,那么您就可以自动从中获益。尽管您一开始可能不愿放弃这么多控制,但要知道这是编程语言和运行时的普遍趋势:随着时代发展,开发人员会越来越远离那些平台能更有效处理的细节。我从不担心JVM的内存管理,因为平台可以让我忘了它。当然,有时候它也会让事情变得更复杂,但是对于您从日复一日的编码中获得的收益来说,这是值得的。函数语言结构,如高阶和一级函数,能让我对抽象的理解更进一步,让我更多地将精力放在代码能做什么而不是怎么做上。
即使使用Functional Java 框架,在Java 中以这种风格编程也很麻烦,因为这种语言并没有真正的这类语法和结构。在支持的语言中进行函数编码是什么样的呢?
Clojure 中的分类器
Clojure 是一种用于JVM 的Lisp(见参考资料)。看看用Clojure 编写的数字分类器,如清单2 所示:
清单2. 数字分类器的Clojure 实现
(use '[clojure.contrib.import-static :only (import-static)])
(import-static java.lang.Math sqrt)
(defn is-factor?[factor number]
(= 0 (rem number factor)))
(defn factors [number]
(set (for [n (range 1 (inc number)) :when (is-factor? n number)] n)))
(defn sum-factors [number]
(reduce + (factors number)))
(defn perfect?[number]
(= number (- (sum-factors number) number)))
(defn abundant?[number]
(< number (- (sum-factors number) number)))
(defn deficient?[number]
(> number (- (sum-factors number) number)))
即使您不是熟练的Lisp开发人员,也能轻松读懂清单2中的大多数代码—您可以学着从内向外读。例如,is-factor?
方法有两个参数,它判断number
除以factor
时余数是否等于零。同样,perfect?
、abundant?
和deficient?
方法也很容易理解,尤其是参考一下清单1的Java实现之后。
sum-factors
方法使用内置的reduce
方法。sum-factors
一次减少一个列表元素,它使用函数(本例中,是+
)作为每个元素的第一个参数。reduce
方法在几种语言和框架中会有不同的形式;清单1是foldLeft()
方法的Functional Java版本。factors
方法会返回一个数字列表,因此我一次处理一个数字,将每个元素加入和中,该和数就是 reduce
的返回值。您会看到,一旦您习惯了从高阶和一级函数的角度思考,您就能减少(一语双关)代码中无用的部分。
factors
方法可能看上去像一个随机符号集。但如果您理解了列表的含义,您就知道这么做是有道理的,这是Clojure中强大的列表操作功能之一。和之前一样,从内向外阅读factors
就很容易理解。不要被这些混在一起的语言术语搞糊涂。Clojure中的for
关键词并不表示for
循环。相反,将它当成是所有过滤和转换结构的来源。本例中,我让它过滤从1到(number
+ 1)范围的数字,使用is-factor?
谓词(我在之前的清单2中定义的is-factor
方法—请注意一类函数的大量使用),返回匹配的数字。从此操作返回的是一组满足过滤标准的数字列表,我将其放入一个集合来删除重复值。
尽管学习一门新的语言很麻烦,但对于函数语言,当您了解其特点后,学起来就容易得多。
优化
转换到函数样式的收益之一就是能利用语言或框架提供的高阶函数。那么不想放弃控制的时候呢?我在之前的例子中,把遍历机制的内部行为比作内存管理器的内部运作:大多数时候,您会很高兴不用关心那些细节。但有时候您会关心这些问题,特别是在遇到优化或类似任务时。
在“运用函数式思维,第1部分”的数字分类器的两个Java版本中,我优化了确定因子的代码。原先是简单的使用取模运算符(%
)的实现,它非常低效,它自己检查从2到目标数的每个数字,确定是否是因子。因子是成对出现的,可以通过这点来优化算法。例如,如果您查找28的因子,当您找到2时,那么同时会找到14。如果您成对获取因子,您只需要检查到目标数的平方根即可。
在Java 版本中很容易完成的实现似乎在Functional Java 版本中很难做到,因为我无法直接控制遍历机制。但作为函数式思维的一部分,您需要放弃这种控制观念,学会用另一种控制。
我会以函数式思维重新说明原来的问题:过滤所有1到number
的因子,只保留匹配isFactor()
谓词的因子。其实现见清单3:
清单3. isFactor()
方法
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
尽管看上去很优雅,但清单3 中的代码效率很低,因为它会检查每个数。在了解优化(成对获取因子,只检查到平方根)之后,重述问题如下:
- 过滤目标数的所有因子,从1 到其平方根。
- 用这些因子除以目标数,以获得对称因子,并将它加入因子列表中。
记住了这个目标,我就可以用Functional Java库写出factorsOf()
方法的优化版本,如清单4所示:
清单4. 优化的因子查找方法
List<Integer> factors =
range(1, (int) round(sqrt(number)+1))
.filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}});
return factors.append(factors.map(new F<Integer, Integer>() {
public Integer f(final Integer i) {
return number / i;
}}))
.nub();
}
清单4中的代码是基于我之前讲过的算法,其中有一些独特的语法,这是Functional Java框架所必需的。首先,获取数的范围是从1到目标数的平方根加1(确保能取到所有因子)。第二步,根据与之前版本一样的取模操作方法过滤结果,这些都包含在Functional Java代码段中。我将过滤后的列表放在factors
变量中。第四步(从内到外阅读),获取因子列表,并执行map()
函数,它在代码中对每个元素进行处理(将每个元素映射到一个新值),从而产生一个新的列表。因子列表中包含到目标数平方根的所有因子;需要除以每个数以获得对称因子,而map()
方法就是完成这个任务的。第五步,现在已经有了对称因子列表,我将它添加到原来的列表中。最后一步,有个情况我必需考虑,因子保存在List
中,而不是Set
中。List
方法对于这些类型操作很方便,但我的算法有个副作用,就是出现整数平方根时,会有重复。例如,如果目标数是16,平方根的整部部分4会在 因子列表中出现两次。为了能继续使用方便的List
方法,我只要在最后调用nub()
方法,它将删除所有重复值。
一般情况下在使用高级抽象,比如函数编程时,您不需要了解实现细节,但这并不意味这在必要的情况下,就无法了解。Java 平台大多数情况下不需要您知道底层内容,但如果您下定决心,您就可以了解到你想达到的层次的内容。同样,在函数编程结构中,您可以把细节留给抽象机制,在出现问题的时候才去关注它。
到目前为止所演示的Functional Java 代码中,最精华的部分是代码的语法,它使用了泛型和匿名内部类作为伪代码段和闭包类型结构。闭包是函数语言的共有特征之一。为什么它们用处这么大?
回页首
闭包有什么特别之处?
闭包是一个会对它内部引用的所有变量进行隐式绑定的函数。换句话说,这个函数(或方法)会对它引用的所有内容关闭上下文。闭包经常会在函数语言和框架中用作可移动执行机制,会作为转换代码传递给高阶函数,如map()
。Functional Java使用匿名内部闭包来模仿“实际”闭包行为,但不能一直这样做,因为Java不支持闭包。这意味着什么?
清单5 是一个样例,演示了闭包的特别之处。这是用Groovy 编写的,它通过代码段块机制支持闭包。
清单5. Groovy 代码演示闭包
def very_local_variable = 0
return { return very_local_variable += 1 }
}
c1 = makeCounter()
c1()
c1()
c1()
c2 = makeCounter()
println "C1 = ${c1()}, C2 = ${c2()}"
// output:C1 = 4, C2 = 1
makeCounter()
方法首先定义了一个具有适当名称的本地变量,然后返回使用该变量的代码段。请注意,makeCounter()
方法的返回类型是代码段,而不是值。代码段做的事就是增加本地变量的值并返回。我在代码中设置了显式return
调用,这在Groovy中都是可选的,但如果不用,代码就更难懂了!
为了能使用makeCounter()
方法,我将代码段指定为C1
变量,然后调用三次。我使用了Groovy的语法糖(syntactic sugar)来执行此代码段,结果放置在代码段变量旁边的圆括号中。接下来,我再次调用makeCounter()
,将代码段的一个新实例指定为C2
。最后,我再次执行C1
和C2
。请注意,每个代码段都能追踪到very_local_variable
的一个单独实例。这就是封闭的上下文的含义。即使在方法内部定义本地变量,代码段仍会绑定到该变量,因为变量引用了代码段,这意味着在代码段可用时,变量必须能追踪到它。
在Java 中实现相同操作的最简单的方法如清单6:
清单6. Java中的Counter
private int varField;
public Counter(int var) {
varField = var;
}
public static Counter makeCounter() {
return new Counter(0);
}
public int execute() {
return ++varField;
}
}
对Counter
类进行一些变化都可以,但您一定要自己管理状态。以上演示了为什么闭包体现了函数思想:让运行时管理状态。与其强迫自己处理字段创建和原始状态(包括在多线程环境中使用代码的可怕前景),还不如让语言或框架默默地管理状态。
在以后的Java 版本中会有闭包(关于本话题的讨论已超出本文范围)。Java 中包含它们将有两项收益。首先,在改进语法的同时,大大简化框架和库作者的能力。第二,为所有在JVM 上运行的语言提供一个底层的共同基础。即使很多JVM 语言支持闭包,但它们都实现自己的版本,这造成在语言之间传递闭包非常麻烦。如果Java 定义了一个单一格式,那么其他所有语言都能利用它。
回页首
结束语
回避对底层细节的控制是软件开发的普遍趋势。我们很高兴不用再操心垃圾回收、内存管理和硬件差异。函数编程代表了下一个抽象阶段:将更加琐碎的细节如遍历、并发性和状态尽可能留给运行时处理。这并不意味着您在需要的时候无法再控制它们— 是您想要控制,而不是被迫控制。
在下一篇文章中,我将继续探讨Java及同类语言中的函数编程结构,我将会介绍currying和partial method application。