在本系列的第一部分中,我开始讨论函数式编程的一些特征,展示了这些思想在Java语言和更多函数式语言中如何体现。 在本文中,我将通过讨论一流的函数,优化和闭包来继续概念之旅。 但是本部分的基本主题是控制 :何时需要,何时需要以及何时放手。
一流的功能和控制
使用功能的Java库(参见相关主题 ),我最后表现出与功能性的一些分类的实施isFactor()
和factorsOf()
方法,如清单1所示:
清单1.数字分类器的功能版本
import fj.F;
import fj.data.List;
import static fj.data.List.range;
import static fj.function.Integers.add;
import static java.lang.Math.round;
import static java.lang.Math.sqrt;
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
时,余number
是否等于0。 同样, perfect?
abundant?
和deficient?
方法应该易于理解,特别是如果您参考清单1中的Java实现。
sum-factors
方法使用内置的reduce
方法。 sum-factors
使用作为每个元素上的第一个参数提供的函数(在本例中为+
)一次减少列表中的一个元素。 reduce
方法以几种语言和框架以不同的形式出现; 您在清单1的Functional Java版本中将其foldLeft()
方法。 factors
方法返回一个数字列表,因此我一次处理一个列表,将每个元素加到累加和中,这是reduce
的返回值。 您会看到,一旦习惯了对高阶和一流函数的思考 ,就可以减少(双关语意)代码中的大量杂音。
factors
方法似乎是符号的随机集合。 但是,一旦您了解了列表理解功能(Clojure中几个强大的列表操作功能之一),它的确有意义。 和以前一样,从内到外理解factors
是最容易的。 不要被冲突的语言术语所迷惑。 Clojure中的for
关键字并不表示for
循环。 而是将其视为所有过滤和转换构造的祖父。 在这种情况下,我要求它使用is-factor?
过滤从1到( number
+ 1)的数字范围is-factor?
谓词(这是我在清单2前面定义的is-factor
方法-注意大量使用一流的函数),返回匹配的数字。 此操作的返回结果是符合筛选条件的数字列表,我将其强制转换为集合以除去重复项。
尽管学习新语言很麻烦,但是当您了解功能语言时,就会从功能语言中受益匪浅。
优化
切换到功能样式的好处之一是能够利用语言或框架提供的高阶功能支持。 但是,当您不想放弃控制权时,情况又如何呢? 在我之前的示例中,我将迭代机制的内部行为比作内存管理器的内部工作:大多数时候,您很乐意不担心那些细节。 但是有时候,您确实会关心它们,例如优化和类似的调整。
在“ 功能性思考,第1部分 ”中显示的数字分类器的两个Java版本中,我优化了确定因素的代码。 最初的幼稚实现使用效率极低的模数( %
)运算符来检查从2到目标数字本身的每个数字,以确定它是否是一个因数。 您可以通过注意因素成对来优化算法。 例如,如果您正在寻找28的因数,那么当您找到2时,您也可以抓住14。 如果可以成对收集因子,则只需检查不超过目标数平方根的因子。
在Java版本中易于执行的优化在Functional Java版本中似乎是不可能的,因为我无法直接控制迭代机制的实现。 但是,学习功能性思考的一部分需要放弃关于这种控制的概念,从而使您能够发挥另一种控制作用。
我可以从功能上isFactor()
原始问题:过滤从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到数字的平方根。
- 将目标数量除以这些因子中的每一个,即可得到对称因子,并将其添加到因子列表中。
考虑到这一目标,我可以使用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;
}}))
.nub();
}
清单4中的代码基于我先前所述的算法,以及Functional Java框架所需的一些时髦语法。 首先,我将数字范围从1到目标数字的平方根加1(以确保我能抓住所有因素)。 第二,我像包装在Functional Java代码块中一样,使用模数运算符(如先前版本)过滤结果。 我将此过滤后的列表保存在factors
变量中。 第四(从内到外读取),我获取了这个因子列表并执行map()
函数,该函数通过对每个元素执行我的代码块( 将每个元素映射到新值)来生成一个新列表。 我的因子列表包含目标数直至其平方根的所有因子; 我需要将每个目标数除以目标数以获得对称系数,这就是发送到map()
方法的代码块所执行的操作。 第五,现在我有了对称因子的列表,我将其添加到原始列表中。 作为最后一步,我必须说明我将因子保留在List
而不是Set
的事实。 List
方法对于这些类型的操作很方便,但是当弹出整数平方根时,我的算法的副作用是重复条目。 例如,如果目标数为16,则整数根4会两次出现在因子列表中。 为了继续使用便捷的List
方法,我只需要在最后调用其nub()
方法即可,该方法将删除所有重复项。
仅仅因为您通常在使用函数式编程等更高级别的抽象时就放弃了实现细节的知识,并不意味着您在必须做到的情况下不会陷入困境。 Java平台通常会使您免受低级内容的侵害,但是如果您下定决心,则可以深入到所需的级别。 同样,在函数式编程构造中,您通常愿意将细节放给抽象,从而保留真正重要的时间。
到目前为止,我所展示的所有Functional Java代码中视觉上的突出表现都是块语法,它使用泛型和匿名内部类作为伪代码块,闭包类型构造。 闭包是功能语言的常见功能之一。 是什么使它们在这个世界上如此有用?
闭包有什么特别之处?
闭包是一种对其中引用的所有变量进行隐式绑定的函数。 换句话说,函数(或方法)在其引用的内容周围包含一个上下文。 闭包在功能语言和框架中经常用作可移植的执行机制,并传递给诸如map()
类的高阶函数作为转换代码。 函数式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的语法糖来执行代码块,该代码块是在代码块的变量旁边放置一组括号。 接下来,我再次调用makeCounter()
,将代码块的新实例分配给C2
。 最后,我再次执行C1
和C2
。 请注意,每个代码块都跟踪了very_local_variable
的单独实例。 这就是封闭上下文的意思。 即使在方法中定义了局部变量,代码块仍会绑定到该变量,因为它引用了该变量,这意味着在代码块实例处于活动状态时,它必须跟踪该变量。
清单6中出现了与Java中可能发生的最接近的相同行为:
清单6. Java中的MakeCounter
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语言定义了一种格式,则所有其他语言都可以利用它。
结论
放弃对低级细节的控制是软件开发的普遍趋势。 我们很高兴地放弃了垃圾回收,内存管理和硬件差异的责任。 函数式编程代表了下一个抽象飞跃:将更多平凡的细节(例如迭代,并发和状态)分配给运行时。 这并不意味着您无法在必要时收回控制权,但是您必须要拥有控制权,而不是将其强加于您。
在下一部分中,我将通过介绍currying和Partial Method Application继续探索Java和近亲中的函数式编程构造。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft2/index.html