• 函数式思维: 运用函数式思维,第2 部分


    关于本系列

    本系列文章旨在将您的思维方式向函数式思维方式调整,使您以全新的角度来思考常见问题,并提高您的日常编码工作。本系列介绍了函数式编程概念,函数式编程在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

  • 相关阅读:
    Flexbox兼容性
    响应式布局(Responsive Layout)/流式布局(Fluid Layout)/自适应布局(Adaptive)
    Flex布局之box-flex
    Flex布局(伸缩盒布局)
    input美化问题
    用纯css改变下拉列表select框的默认样式
    超越icon font
    CSS居中之美
    php中yaf框架的服务器配置
    MySQL优化
  • 原文地址:https://www.cnblogs.com/chenying99/p/2723384.html
Copyright © 2020-2023  润新知