假设您是一名伐木工人。 您拥有森林中最好的斧头,这使您成为营地中生产力最高的伐木工人。 然后有一天,有人露面并颂扬一种新的砍树范例电锯的优点。 销售人员很有说服力,因此您购买了电锯,但您不知道它是如何工作的。 您尝试将其举起来并用力在树上摆动,这是您其他砍树范例的工作方式。 您很快就得出结论,这种新奇的电锯只是一种时尚,然后您又回到用斧头砍伐树木的方式。 然后,有人走过去,向您展示如何摇链锯。
您可能与这个故事有关,但是使用函数式编程而不是电锯 。 全新的编程范例所带来的问题不是学习新的语言。 毕竟,语言语法只是细节。 棘手的部分是学会以不同的方式思考 。 那就是我进来的地方-电锯曲柄和功能程序员。
欢迎来到功能思维 。 本系列探讨函数式编程的主题,而不仅仅是函数式编程语言。 正如我将说明的那样,以“功能”方式编写代码涉及设计,权衡,不同的可重用构建基块以及许多其他见解。 我将尽可能地尝试以Java(或接近Java的语言)展示函数式编程的概念,并转向其他语言以展示Java语言中尚不存在的功能。 我不会跳下深水和谈论时髦之类的东西的单子(参见相关主题 )马上(虽然我们会到达那里)。 取而代之的是,我将逐步向您展示思考问题的新方法(您已经在某些地方应用了该方法-您还没有意识到)。
本期以及接下来的两期将带旋风中一些与函数式编程相关的主题,包括核心概念。 当我在整个系列文章中建立更多的上下文和细微差别时,其中一些概念将更为详细。 作为本教程的出发点,我将带您研究问题的两种不同实现,一种是命令式编写的,另一种是功能更强大的。
数字分类器
要谈论不同的编程风格,您必须具有用于比较的代码。 我的第一个示例是我在《生产程序员》 (请参阅参考资料 )和《 测试驱动的设计,第1部分 》和《 测试驱动的设计,第2部分 》(我上一期的两期)中出现的一个编码问题的变体。 developerWorks系列演化架构和紧急设计 )。 我之所以选择此代码,至少是部分原因是因为这两篇文章深入介绍了该代码的设计。 这些文章中提到的设计没有错,但是在这里我将提供一种不同设计的理由。
要求指出,给定任何大于1的正整数,您必须将其分类为perfect , 丰富或不足 。 理想数是一个因数(不包括因数本身引起的影响)加起来的数字。 同样,数量丰富的因子之和大于数量,数量不足的因子之和更少。
祈使数字分类器
清单1中显示了一个满足这些要求的命令式类:
清单1. NumberClassifier
,这个问题的必要解决方案
public class Classifier6 {
private Set<Integer> _factors;
private int _number;
public Classifier6(int number) {
if (number < 1)
throw new InvalidNumberException(
"Can't classify negative numbers");
_number = number;
_factors = new HashSet<Integer>>();
_factors.add(1);
_factors.add(_number);
}
private boolean isFactor(int factor) {
return _number % factor == 0;
}
public Set<Integer> getFactors() {
return _factors;
}
private void calculateFactors() {
for (int i = 1; i <= sqrt(_number) + 1; i++)
if (isFactor(i))
addFactor(i);
}
private void addFactor(int factor) {
_factors.add(factor);
_factors.add(_number / factor);
}
private int sumOfFactors() {
calculateFactors();
int sum = 0;
for (int i : _factors)
sum += i;
return sum;
}
public boolean isPerfect() {
return sumOfFactors() - _number == _number;
}
public boolean isAbundant() {
return sumOfFactors() - _number > _number;
}
public boolean isDeficient() {
return sumOfFactors() - _number < _number;
}
public static boolean isPerfect(int number) {
return new Classifier6(number).isPerfect();
}
}
有关此代码的几件事值得注意:
- 它具有广泛的单元测试(部分原因是我为讨论测试驱动的开发而编写)。
- 该类包含大量的衔接方法,这是在其构造中使用测试驱动的开发的副作用。
- 性能优化嵌入到
calculateFactors()
方法中。 该类别的内容包括收集因素,因此我可以对其进行汇总并最终对其进行分类。 因子总是可以成对收获。 例如,如果所讨论的数字为16,则当我抓住因子2时,我也可以抓住8,因为2 x 8 =16。如果我成对收获因子,则只需要检查因子的平方根即可。目标数字,这恰好是calculateFactors()
方法的作用。
(略多)功能分类器
使用相同的测试驱动开发技术,我创建了分类器的替代版本,如清单2所示:
清单2.功能性稍强的数字分类器
public class NumberClassifier {
static public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
static public Set<Integer> factors(int number) {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(number, i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
static public int sum(Set<Integer> factors) {
Iterator it = factors.iterator();
int sum = 0;
while (it.hasNext())
sum += (Integer) it.next();
return sum;
}
static public boolean isPerfect(int number) {
return sum(factors(number)) - number == number;
}
static public boolean isAbundant(int number) {
return sum(factors(number)) - number > number;
}
static public boolean isDeficient(int number) {
return sum(factors(number)) - number < number;
}
}
这两个版本的分类器之间的差异很细微但很重要。 主要区别在于清单2中故意缺乏共享状态。 消除(或至少减少)共享状态是函数编程中最受欢迎的抽象之一。 我没有在方法之间共享状态作为中间结果(请参见清单1中的factors
字段),而是直接调用方法以消除状态。 从设计的角度来看,它使factors()
方法更长,但可以防止factors
字段“泄漏”该方法。 还要注意, 清单2版本可能完全由静态方法组成。 这些方法之间没有共享的知识,因此我不需要通过作用域进行封装。 如果为它们提供期望的输入参数类型,那么所有这些方法都可以很好地工作。 (这是一个纯函数的示例,我将在以后的文章中进一步研究这个概念。)
功能
函数式编程是计算机科学的一个广泛而广泛的领域,最近引起了人们的关注。 新型功能性语言的JVM(如Scala和Clojure的)和框架(如Java功能和阿卡),是在现场(见相关信息 ),以更少的错误通常的索赔一起,更高的生产效率,更好的外观,更多的钱,等等。 我不会尝试从根本上解决功能编程的整个主题,而是将重点放在几个关键概念上,并从这些概念中得出一些有趣的含义。
函数编程的核心是函数 ,就像类是面向对象语言中的主要抽象一样。 功能构成了处理的基础,并充满了传统命令式语言所没有的一些功能。
高阶函数
高阶函数可以将其他函数作为参数,也可以将它们作为结果返回。 我们没有Java语言中的此构造。 最接近的方法是将一个类(通常是一个匿名类)用作您需要执行的方法的“持有人”。 Java没有独立的函数(或方法),因此它们不能从函数返回或作为参数传递。
出于至少两个原因,此功能在功能语言中很重要。 首先,具有高阶函数意味着您可以假设语言部分如何组合在一起。 例如,可以通过建立遍历列表并将一个(或多个)高阶函数应用于每个元素的通用机制,消除类层次结构上方法的整个类别。 (稍后,我将向您展示此结构的示例。)其次,通过将函数用作返回值,您将有机会构建高度动态,适应性强的系统。
通过使用高阶函数可以解决的问题并非功能语言所独有。 但是,从功能上考虑时,解决问题的方式会有所不同。 考虑清单3中的示例(取自较大的代码库),该示例执行受保护的数据访问:
清单3.潜在可重用的代码模板
public void addOrderFrom(ShoppingCart cart, String userName,
Order order) throws Exception {
setupDataInfrastructure();
try {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
completeTransaction();
} catch (Exception condition) {
rollbackTransaction();
throw condition;
} finally {
cleanUp();
}
}
清单3中的代码执行初始化,执行一些工作,如果一切成功,则完成事务,否则回滚,最后清理资源。 显然,此代码的样板部分可以重复使用,我们通常通过创建结构以面向对象的语言来重复使用。 在这种情况下,我将结合四个设计模式中的两个(请参阅参考资料 ):模板方法和命令模式。 模板方法模式建议我将通用的样板代码上移到继承层次结构中,将算法细节推迟到子类中。 Command设计模式提供了一种将行为封装在具有众所周知的执行语义的类中的方法。 清单4显示了将这两种模式应用于清单3中的代码的结果:
清单4.重构的订单代码
public void wrapInTransaction(Command c) throws Exception {
setupDataInfrastructure();
try {
c.execute();
completeTransaction();
} catch (Exception condition) {
rollbackTransaction();
throw condition;
} finally {
cleanUp();
}
}
public void addOrderFrom(final ShoppingCart cart, final String userName,
final Order order) throws Exception {
wrapInTransaction(new Command() {
public void execute() {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
}
});
}
在清单4中 ,我将代码的通用部分提取到wrapInTransaction()
方法(您可能会认识到它的语义—它基本上是Spring的TransactionTemplate
的简单版本),并将Command
对象作为工作单元传递。 addOrderFrom()
方法折叠到命令类的匿名内部类创建的定义,包装了两个工作项。
在命令类中包装我需要的行为,纯粹是Java设计的产物,它不包括任何独立行为。 Java中的所有行为都必须驻留在一个类中。 甚至语言设计师也很快发现了这种设计的不足之处-事后看来,认为永远都不会有不属于班级的行为是天真的想法。 JDK 1.1通过添加匿名内部类来纠正此缺陷,该内部类至少提供了语法糖,仅使用一些纯粹是功能性而非结构性的方法即可创建许多小型类。 有关Java的这一方面的有趣且幽默的文章,请查看Steve Yegge的“在名词王国中执行”(请参阅参考资料 )。
Java强迫我创建Command
类的实例,即使我真正想要的只是该类内部的方法。 该类本身没有任何好处:它没有字段,没有构造函数(从Java自动生成的构造函数除外),也没有状态。 它纯粹用作方法内部行为的包装。 在功能语言中,这将通过更高阶的函数来处理。
如果我愿意暂时离开Java语言,可以使用闭包在语义上接近函数式编程的理想状态。 清单5显示了相同的重构示例,但是使用Groovy(请参阅参考资料 )代替Java:
清单5.使用Groovy闭包代替命令类
def wrapInTransaction(command) {
setupDataInfrastructure()
try {
command()
completeTransaction()
} catch (Exception ex) {
rollbackTransaction()
throw ex
} finally {
cleanUp()
}
}
def addOrderFrom(cart, userName, order) {
wrapInTransaction {
add order, userKeyBasedOn(userName)
addLineItemsFrom cart, order.getOrderKey()
}
}
在Groovy中,花括号{}
中的任何内容都是代码块,并且代码块可以作为参数传递,从而模仿了高阶函数。 在后台,Groovy正在为您实现Command设计模式。 Groovy中的每个闭包块实际上都是Groovy闭包类型的实例,该实例包括一个call()
方法,当您在保存闭包实例的变量后放置一个空括号时会自动调用该方法。 Groovy通过将适当的数据结构和相应的语法糖构建到语言本身中,实现了某些类似于函数编程的行为。 正如我将在以后的文章中展示的那样,Groovy还包括Java之外的其他功能编程功能。 在后面的部分中,我还将循环讨论闭包和高阶函数之间的一些有趣比较。
一流的功能
功能语言中的功能被视为第一类 ,这意味着功能可以出现在任何其他语言构造(例如变量)可以出现的任何位置。 一流功能的存在允许以意外的方式使用功能,并迫使人们以不同的方式考虑解决方案,例如将相对通用的操作(具有细微差别的细节)应用于标准数据结构。 反过来,这暴露了功能语言思维的根本转变: 关注结果而不是步骤。
在命令式编程语言中,我必须考虑算法中的每个原子步骤。 清单1中的代码显示了这一点。 为了解决数字分类器,我必须准确地辨别如何收集因子,这又意味着我必须编写特定的代码来遍历数字以确定因子。 但是遍历列表,对每个元素执行操作,听起来很普通。 考虑使用清单6中出现的功能Java框架重新实现的数字分类代码:
清单6.功能编号分类器
public class FNumberClassifier {
public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
public List<Integer> factors(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return isFactor(number, i);
}
});
}
public int sum(List<Integer> factors) {
return factors.foldLeft(fj.function.Integers.add, 0);
}
public boolean isPerfect(int number) {
return sum(factors(number)) - number == number;
}
public boolean isAbundant(int number) {
return sum(factors(number)) - number > number;
}
public boolean isDeficiend(int number) {
return sum(factors(number)) - number < number;
}
}
清单6和清单2之间的主要区别在于两个方法: sum()
和factors()
。 sum()
方法利用了Functional Java中List
类的一种方法,即foldLeft()
方法。 这是对列表处理概念(称为catamorphism )的特定变体,它是对列表折叠的概括。 在这种情况下,“向左折”表示:
- 取一个初始值,然后通过对列表的第一个元素进行操作将其合并。
- 得到结果并将相同的操作应用于下一个元素。
- 继续执行此操作,直到列表用尽。
请注意,这正是对数字列表求和时要执行的操作:从零开始,添加第一个元素,获取结果并将其添加到第二个元素,然后继续直到列表被消耗为止。 Functional Java提供了高阶函数(在此示例中为Integers.add
枚举),并会为您应用该函数。 (当然,Java并没有真正的高阶函数,但是如果您将其限制为特定的数据结构和类型,则可以编写一个不错的类比。)
清单6中的另一个有趣的方法是factors()
,它说明了我的“关注结果而不是步骤”建议。 发现数量因素的问题的本质是什么? 换句话说,给定所有可能的数字列表,直到目标数字,我如何确定哪些是数字因素? 这建议进行过滤操作-我可以过滤整个数字列表,排除那些不符合我的条件的数字。 该方法基本上像这样描述:将数字范围从1到我的数字(范围是非包含性的,因此是+1
); 根据f()
方法中的代码过滤列表,这是Functional Java允许您创建具有特定数据类型的类的方法; 和返回值。
该代码还说明了一个更大的概念,以及一般编程语言的趋势。 过去,开发人员必须处理各种烦人的事情,例如内存分配,垃圾回收和指针。 随着时间的流逝,语言承担了更多的责任。 随着计算机变得越来越强大,我们已经将越来越多的普通(可自动化)任务卸载到了语言和运行时中。 作为Java开发人员,我已经习惯于将所有内存问题都割让给该语言。 函数式编程正在扩展该任务,包括更多特定的细节。 随着时间的流逝,我们将花费更少的时间来担心解决问题所需的步骤 ,并更多地考虑流程 。 随着本系列的进行,我将展示许多示例。
结论
功能编程比一组特定的工具或语言更像是一种思维方式。 在第一部分中,我开始介绍函数式编程中的一些主题,从简单的设计决策到雄心勃勃的问题重新思考。 我重写了一个简单的Java类以使其更具功能性,然后开始研究一些使功能编程与使用传统命令性语言不同的主题。
这里首先出现了两个重要的,长期的概念。 首先,注重结果,而不是步骤。 函数式编程会尝试以不同的方式提出问题,因为您拥有促进解决方案的不同构建基块。 我将在本系列中展示的第二个趋势是将平凡的细节转移到编程语言和运行时,这使我们能够专注于编程问题的独特方面。 在下一部分中,我将继续研究函数式编程的一般方面以及它如何应用于当今的软件开发。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft1/index.html