君子博学而日参省乎己 则知明而行无过矣

博客园 首页 新随笔 联系 订阅 管理

关于本系列

本系列文章旨在将您的思维方式向函数式思维方式调整,使您以全新的角度来思考常见问题,并提高您的日常编码工作。本系列介绍了函数式编程概念,函数式编程在Java 语言中运行的框架、在JVM 上运行的函数式编程语言、以及语言设计未来的一些方向。本系列主要面向了解Java 以及其抽象层的工作方式,但缺乏函数式语言使用经验的开发人员。

在本系列的第一期中,我首先讨论函数编程的一些特点,并演示如何在Java和其他函数语言中体现这些观念。在本文中,我将继续讨论这些概念,讲解一级函数、优化和闭包。但本期的内在主题是控制:什么时候想要控制、什么时候需要控制、什么时候应该放弃控制。

一级(First-class)函数和控制

我在上一部分最后通过使用Functional Java库(见参考资料),演示了用函数isFactor()factorsOf()方法实现数字分类器,如清单1所示:


清单1. 数字分类器的函数版本

public class FNumberClassifier {

    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 实现

(ns nealford.perfectnumbers)
(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()方法

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;
        }
    });

尽管看上去很优雅,但清单3 中的代码效率很低,因为它会检查每个数。在了解优化(成对获取因子,只检查到平方根)之后,重述问题如下:

  1. 过滤目标数的所有因子,从1 到其平方根。
  2. 用这些因子除以目标数,以获得对称因子,并将它加入因子列表中。

记住了这个目标,我就可以用Functional Java库写出factorsOf()方法的优化版本,如清单4所示:


清单4. 优化的因子查找方法

public List<Integer> factorsOfOptimzied(final int number) {
    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;
                                      }}))
                                      .nu​​b();

清单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 makeCounter() {
  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。最后,我再次执行C1C2。请注意,每个代码段都能追踪到very_local_variable的一个单独实例。这就是封闭的上下文的含义。即使在方法内部定义本地变量,代码段仍会绑定到该变量,因为变量引用了代码段,这意味着在代码段可用时,变量必须能追踪到它。

在Java 中实现相同操作的最简单的方法如清单6:


清单6. Java中的Counter 

public class 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及同类语言中的函数编程结构,我将会介绍curryingpartial method application

posted on 2012-10-14 21:15  刺猬的温驯  阅读(245)  评论(0编辑  收藏  举报