插入大量

    技术2024-03-25  95

    关于本系列

    本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许在Java语言中进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。

    功能编程语言对代码重用的方式与面向对象的语言不同,这是我在“ 耦合和组合,第2部分 ”中研究的主题。 面向对象的语言倾向于具有许多具有许多操作的数据结构,而功能语言则很少具有具有许多操作的数据结构。 面向对象的语言鼓励您创建特定于类的方法,并且可以捕获重复模式以供以后重用。 功能语言通过鼓励将常见的转换应用于数据结构来帮助您实现重用,并使用高阶函数针对特定实例自定义操作。

    相同的数据结构和操作在所有功能语言中以及在许多支持Java功能编程的框架中均会出现,但是它们的名称常常不同。 混淆的命名使得即使基础概念相同,也很难将知识从一种语言翻译成另一种语言。

    本期文章的目的是促进翻译。 我解决了一个简单的问题,该问题需要决策和迭代,并以五种语言(Java,Groovy,Clojure,JRuby和Scala)和两个针对Java的功能框架(Functional Java和Totally Lazy)来实现解决方案(请参阅参考资料 )。 实现是相同的,但是不同语言之间的细节差异很大。

    纯Java

    问题在于确定整数是否是质数—仅因数为1的质数。 存在几种确定质数的算法(一些替代方案出现在“ 耦合和组成,第1部分 ”中); 我将使用一种确定数字因子的解决方案,然后检查因子之和是否等于数字加1,表示数字为质数。 这不是最有效的算法,但我的目标是展示常见收集方法的不同实现,而不是效率。

    清单1中显示了纯Java版本:

    清单1.普通的Java质数分类器
    public class PrimeNumberClassifier { private Integer number; public PrimeNumberClassifier(int number) { this.number = number; } public boolean isFactor(int potential) { return number % potential == 0; } public Set<Integer> getFactors() { Set<Integer> factors = new HashSet<Integer>(); factors.add(1); factors.add(number); for (Integer i = 2; i < number; i++) if (isFactor(i)) factors.add(i); return factors; } public int sumFactors() { int sum = 0; for (int i : getFactors()) sum += i; return sum; } public boolean isPrime() { return sumFactors() == number + 1; } }

    如果您阅读过以前的Functional Thinking文章 ,您将认识到清单1的getFactors()方法中找到的算法。 它的核心是isFactor()方法,该方法筛选候选因素。

    Groovy

    Groovy在其发展过程中添加了许多功能构造,从而导致实现,如清单2所示,该实现看起来与Java完全不同:

    清单2. Groovy质数分类器
    class PrimeNumberClassifier { private int number; PrimeNumberClassifier(int number) { this.number = number } public def isFactor(int potential) { number % potential == 0; } public def getFactors() { (1..number).findAll { isFactor(it) }.toSet() } public def sumFactors() { getFactors().inject(0, {i, j -> i + j}) } public def isPrime() { sumFactors() == number + 1 } }

    清单1中与之对应的两种方法的变化不仅仅在于清单2中的语法。 第一个是getFactors() ,它使用Groovy的Range类来表示候选编号。 findAll()方法将代码块应用于集合的每个元素,返回一个列表,其中包含该块为其返回true 。 该代码块接受单个参数,即正在检查的元素。 我使用便捷的Groovy速记来简化代码块。 例如,代码块可以写为(1..number).findAdd {i-> isFactor(i) } ,但是重复单个参数是多余的。 Groovy提供所示的选项清单2与隐更换孤独参数的it 。

    清单2中另一个值得注意的方法是sumFactors()方法。 使用从getFactors()方法生成的一组数字,我调用inject() ,这是执行折叠操作的Groovy收集方法。 inject()方法使用第二个参数中提供的代码块(使用第一个参数作为初始种子值inject()组合集合中的每个元素。 清单2中的代码块参数是{i, j-> i + j} ,它返回两个数字的和。 inject()方法将此块从第一个元素开始,依次应用于每个元素,对数字列表求和。

    将函数方法与高阶函数结合使用有时会导致代码密集。 即使清单2中的每个方法都是一行,将它们分解为单独的方法仍然是有益的。 按功能将方法分开,在当前问题的上下文中为每个方法赋予一个有意义的名称,使推理变得更容易。

    斯卡拉

    素数分类器的Scala版本显示在清单3中:

    清单3. Scala质数分类器
    object PrimeNumberClassifier { def isFactor(number: Int, potentialFactor: Int) = number % potentialFactor == 0 def factors(number: Int) = (1 to number) filter (isFactor(number, _)) def sum(factors: Seq[Int]) = factors.foldLeft(0)(_ + _) def isPrime(number: Int) = sum(factors(number)) == number + 1 }

    除了要短得多以外,Scala版本在许多其他方面看起来也有所不同。 因为只需要一个实例,所以使它成为object而不是class 。 factors()方法使用与清单2中的Groovy版本相同的实现,但是语法不同。 我使用清单3开头定义的isFactor()方法作为谓词, 将 filter (Groovy的findAll() Scala版本)应用于数字范围(1 to number) 。 Scala也允许参数占位符-在这种情况下为_ 。

    清单3中的sum()方法使用Scala的foldLeft()方法,它与Groovy的inject()同义。 在这种情况下,我将零用作种子值,并将占位符用于两个参数。

    Clojure

    Clojure是JVM上Lisp的现代实现,从而导致清单4中出现了截然不同的语法:

    清单4. Clojure质数分类器
    (ns prime) (defn factor? [n, potential] (zero? (rem n potential))) (defn factors [n] (filter #(factor? n %) (range 1 (+ n 1)))) (defn sum-factors [n] (reduce + (factors n))) (defn prime? [n] (= (inc n) (sum-factors n)))

    清单4中的所有方法对于Java开发人员来说都是陌生的,但是代码实现了我一直使用的相同算法。 (factor?)方法检查是否余数(Clojure中的rem函数)为零。 (factors)方法使用Clojure的(filter)方法,该方法接受两个参数。 第一个参数是在每个元素上执行的谓词代码块,期望布尔结果是否通过过滤条件。 #(factor? n %)语法使用Clojure的%替换参数表示Clojure匿名函数。 (filter)函数的第二个参数是要过滤的集合,在这种情况下,范围是1到我的目标数加1; 范围不包括最后一个元素。

    清单4中的(sum-factors)方法使用Clojure的(reduce)方法,这是Groovy的inject()和Scala的foldLeft()的foldLeft() 。 在这种情况下,操作是简单的+运算符,对于Clojure而言,它与接受两个参数并返回结果的任何其他方法都没有区别。

    虽然如果您不习惯该语法,可能会令人生畏,但Clojure版本非常简洁。 就像在Groovy版本中一样,好的函数名称也很重要,即使每个函数都是一行,因为这些行有时也很麻烦。

    Ruby

    JRuby提供了Ruby的JVM实现,并且在其生命周期中还获得了许多功能构造。 考虑清单5中出现的素数分类器的(J)Ruby版本:

    清单5. JRuby质数分类器
    class PrimeNumberClassifier def initialize(num) @num = num end def factor?(potential) @num % potential == 0 end def factors (1..@num).select { |i| factor?(i) } end def sum_factors factors.reduce(0, :+) end def prime? (sum_factors == @num + 1) end end

    清单5中的factors方法添加了Groovy的findAll的select同义词,以及Scala和Clojure的filter方法。 JRuby的一个不错的功能是易于别名的方法,它为在不同上下文中使用的方法提供了更方便的名称。 当然,JRuby为select方法提供了一个名为find_all的别名,但是在习惯用法中并不常见。

    对于清单5中的sum_factors方法,我使用了JRuby的reduce方法,它模仿了其他几种语言。 在JRuby中,就像在Clojure中一样,运算符是带有有趣名称的方法。 Ruby允许我用符号:+指定plus方法的名称。 作为易读性的帮助,Clojure和Ruby都允许我在期望返回布尔值的函数中添加问号。 并且,根据其性质,Ruby包含了一个用于reduce的inject方法别名。

    功能性Java

    为了不遗漏任何仍在使用Java变体的人,几个函数式编程库通过函数构造增强了Java。 功能性Java就是这样的框架。 清单6中显示了使用Java加上Functional Java的素数分类器:

    清单6.功能性Java质数分类器
    public class FjPrimeNumberClassifier { private int number; public FjPrimeNumberClassifier(int number) { this.number = number; } public boolean isFactor(int potential) { return number % potential == 0; } public List<Integer> getFactors() { return range(1, number + 1) .filter(new F<Integer, Boolean>() { public Boolean f(final Integer i) { return isFactor(i); } }); } public int sumFactors() { return getFactors().foldLeft(fj.function.Integers.add, 0); } public boolean isPrime() { return sumFactors() == number + 1; } }

    清单6中的getFactors()方法对range使用filter()方法(在Clojure中也是排它性的,因此在范围定义中为number + 1 )。 由于Java还没有高阶函数,Functional Java通过使用其内置F类的匿名内部类实例,使用泛型对类型进行参数化来作弊。

    与Scala一样,Functional Java也包含foldLeft()方法,在这种情况下,该方法接受预定义的代码块,该代码块将数字和种子值相加。

    完全懒惰

    Totally Lazy是Java的功能库,它向Java添加了许多惰性集合。 惰性数据结构并不预定义元素,而是对规则进行编码,以便在需要时生成下一个值。 清单7中显示了在Totally Lazy中实现的素数分类器:

    清单7.完全惰性的质数分类器
    public class TlPrimeNumberClassifier { private int number; public TlPrimeNumberClassifier(int number) { this.number = number; } public boolean isFactor(Integer potential) { return (number % potential) == 0; } private List<Integer> listOfPotentialFactors() { List<Integer> results = new ArrayList<Integer>(); for (int i = 1; i <= number + 1; i++) results.add(i); return results; } public boolean isPrime() { return (this.number + 1) == Sequences.init(listOfPotentialFactors()) .filter(new Predicate<Integer>() { @Override public boolean matches(Integer other) { return isFactor(other); }}) .foldLeft(0, sum()) .intValue(); } }

    清单7中的isPrime()方法使用Sequences类,并用所有潜在因子(即从1到目标数的所有数字)的列表进行初始化,然后应用其filter()方法。 在Totally Lazy中, filter()方法需要Predicate类的子类,其中许多已经为常见情况实现。 在我的例子,我重写matches()方法,供给我isFactor()方法来确定的包括。 在获得因子列表之后,我使用foldLeft方法,将提供的sum()方法用作折叠操作。

    在清单7所示的示例中, isPrime()方法完成了大部分繁重的工作。 完全懒惰中的所有数据结构都是懒惰的事实有时在合并它们时会增加复杂性。 考虑一下getFactors()方法的版本,如清单8所示:

    清单8.带有惰性迭代器的完全惰性getFactors
    public Iterator<Integer> getFactors() { return Sequences .init(listOfPotentialFactors()) .filter(new Predicate<Integer>() { @Override public boolean matches(Integer other) { return isFactor(other); } }) .iterator(); }

    清单8中 getFactors()方法的返回类型是Iterator<Integer> ,但是它是一个惰性的迭代器,这意味着除非您对其进行迭代,否则该集合将没有值。 这使得惰性收集成为测试的挑战。 考虑清单8所示的Totally Lazy示例的单元测试,如清单9所示:

    清单9.测试完全懒惰的集合
    @Test public void factors() { TlPrimeNumberClassifier pnc = new TlPrimeNumberClassifier(6); List<Integer> expected = new ArrayList<Integer>() {{ add(1); add(2); add(3); add(6); }}; Iterator<Integer> actual = pnc.getFactors(); assertTrue(actual.hasNext()); int count = 0; for (int i = 0; actual.hasNext(); i++) { assertEquals(expected.get(i), actual.next()); count++; } assertTrue(count == expected.size()); }

    对于惰性集合,我必须遍历该集合以检索值,然后确保惰性列表中没有比预期多的元素。

    尽管可以在Totally Lazy中编写与其他版本一样完善的版本,但是您可能会发现自己正在与越来越多的拜占庭式数据结构作斗争,例如<Iterator<Iterator<Number>>> 。

    结论

    在本期中,我揭露了在各种功能语言和框架中同一行为的名称扩散。 这些语言和框架之间的方法名称永远不会协调,但是随着Java运行时添加诸如闭包之类的构造,互操作性将变得更加容易,因为它们可以共享通用的标准表示形式(而不是像Totally Lazy那样需要笨拙的构造,例如<Iterator<Iterator<Number>>> )。

    在下一部分中,我将通过查看map继续研究转换的相似性。


    翻译自: https://www.ibm.com/developerworks/java/library/j-ft16/index.html

    Processed: 0.011, SQL: 9