内容


Java 的函数式之旅:使用 Apache Functor 体验函数式编程

Comments

函数式编程简介

Java 8 的发布, 其中最引人注目的就是其对于 Lambda Expressions 的支持,无论它是不是如范型一样只是某种语法糖而已,但至少它是对函数式编程的某种尝试。其实早在 2004 年,Apache Commons 里就曾经出现过这样的类包,它就是 Apache Functor。就如同函数式语言的历史一样,在很长的时间里你无法注意到它的存在,甚至无法说服自己去尝试使用它,因为从代码的角度看它过于的繁琐和罗嗦。但时间来到了 2014 年,在如今多线程、高并发、高容错的需求下,函数式编程语言似乎又引起了人们的注意,它身上与生俱来的独特的气息,让它更适合如今的业务需求,并且在一些实际的商务领域发挥着越来越重要的角色。

如今,网络上已经有很多的关于函数式编程的文章,里面都大同小异的介绍到它的以下几个特点:

  • 支持闭包和高阶函数;
  • 支持懒惰计算(lazy evaluation);
  • 使用递归作为控制流程的机制;
  • 加强了引用透明性;
  • 没有副作用;

这些特点的详细说明,可以参阅参考资料。

编程类型:命令式编程 & 函数式编程

人们总是将编程按照类别,划分为命令式编程和函数式编程,它们的区别简单的来说就是:

  • 命令式编程强调“问题怎么做”;
  • 函数式编程强调“问题是什么”;

要说清楚这个问题,还需要引入计算模型的概念,也就是关于计算的模式。它概括的分为图灵计算和 Lambda 演算,虽然日后证明这两种计算模式是等价的,但由于我们日常使用的计算机是基于冯. 诺依曼体系,而这种体系也可以认为是图灵计算模型的具体实现。

图灵的计算模型就是图灵机,这是一种类似于有穷自动机或下推自动机的机器,但具有访问一条无界的存储“带子”上任意单元的能力。图灵机通过不断修改其存储带上的单元值,以一种命令式的方式进行计算,就像高级命令式语言通过修改变量的值做计算一样。换句话说,图灵机就是命令式语言的计算模型,通过修改变量的值来影响后续的计算。也就是说命令式语言内部是有状态的,计算的过程也就是状态转换的过程,改变状态的方式就是通过赋值等方式改变存储器中变量的值。

丘奇的计算模型是 Lambda 演算,基于带参表达式的概念。它包括一条变换规则(变量替换)和一条函数定义方式。在 Lambda 演算中,每个表达式都代表一个只有单独参数的函数,这个函数的参数本身也是一个只有单一参数的函数,同时,函数的值是又一个只有单一参数的函数。Lambda 演算强调的是变换规则的运用,而非实现它们的具体机器。Lambda 演算就是函数式语言的计算模型,函数式语言的计算的主要方式是将函数作用与给定参数上,在函数式语言中可以没有命令式语言所必需的变量和赋值语句,一切(包括程序本身)都是从输入到输出的函数。

Java 与函数式编程

由于最新的 Java 版本引入了函数式编程的概念,Java 阵营里函数式的声音似乎越来越响亮。但无论如何我们都应该清楚地认识到这仅仅只是 Java 语言向函数式编程迈出的一小步,也是目前程序届所提倡和推崇的“混合式编程”理念。“混合式编程”并不是将上面说的命令式编程与函数式编程简单的柔和在一起,它倡导的是在已有的语言基础之上,增加对另一种编程模式的某些方面的支持。 

Java 对 Lambda 的支持,在我看来切实的改变了很多人编码的习惯,也在一定的范围内让代码变得更加简洁和高效。无论这样的特性实现是语言层面(JVM)还是框架层面,它毕竟是朝着混合式编程这个方向,或者说有利于这个语言的层面发展。我们在关注这些特性的同时,也应该对比的看看 Java 在 Function Programming 这个方面是否已经有相应的框架,比如 fun4j、Apache Functor 等等,而在这里,本文就着重介绍 Apache Functor,以及它在表达式函数化和条件语句函数化方面的应用。

Apache Functor

在 Apache Commons 的项目列表里,Functor 总是显得非常的“特殊”和另类,按照文档上的定义:Functor 就是一个能够被用来当作对象操作的函数,或者说用于表示某个单一、一般化的函数对象。Functors 对于函数式编程的技术的支持,概括起来包括以下几点:

  • 函数式风格编程
  • 高级函数
  • 函数式迭代
  • 提倡复用和组合模式而不是继承和覆盖
  • 泛化的“callback”和“extension point”接口设计
  • 泛化的“filter”接口设计
  • 运用了大量的设计模式,例如 Vistor、 Strategy、 Chain of Responsibility 等

在深入的理解和学习 Functor 之前,我们需要先弄清楚几个核心概念 – predicates、 functions、 procedures,对于这些概念和词汇的理解有助于我们进一步的了解 Functor 的整体架构。而且围绕着这些概念,也派生出了其他的一些辅助类和接口,下面我们先来看看这几个概念的定义:

Commons Functor defines three general types of functors:
predicates
functors that return a boolean value
functions
functors that return an Object value
procedures
functors that don't return anything

从类结构上看,Functor 是一个抽象的接口,而在其之下又派生出三个接口,它们分别是: NullaryFunctor、UnaryFunctor、BinaryFunctor,对于这三个接口的划分,完全取决于传递的参数的多少,分别是为空函数,一元函数和二元函数。从数学的角度看,“元”这个概念就是用来表示执行函数时所涉及的变量个数,而从面向对象的角度看就是执行方法时所传递的参数的个数。但这些接口都是抽象接口,也就是说在它们内部并没有实际的方法,只是用来代表某种抽象的概念。

如果我们单从函数方法这个角度,就会发现刚才介绍的那三个核心概念,是用来对函数方法的返回结果的抽象,而这里的三个抽象接口是用来对函数方法的传递参数的抽象。如果将它们组合起来,就形成了完整的 Functor 核心接口。比如我们将 Function 和 BinaryFunctor 组合起来就形成了 BinaryFunction,Function 的意思是返回某个对象的函数,而 BinaryFunctor 的意思是二元函数,他们的组合完整的描述了函数的返回结果和传递的参数。

图 1. Apache Functor 的类结构图
图 1. Apache Functor 的类结构图
图 1. Apache Functor 的类结构图

在了解了它的核心概念和相应的接口之后,我们换一个角度并打开 Functor 的 Jar 包,看看它内部是如何分类的(Apache Functor API Doc):

org.apache.commons.functor Basic functor interfaces.
org.apache.commons.functor.adapter Classes that adapt one functor interface to another.
org.apache.commons.functor.aggregator This package contains the interfaces and utilities needed to implement an aggregation service.
org.apache.commons.functor.core Commonly used functor implementations.
org.apache.commons.functor.generator Contains code related to Generators.
org.apache.commons.functor.range Contains code related to Ranges.

本文作为 Functor 的介绍文章,无法涵盖所有类的具体使用。所以我们从一个具体的实例出发,让大家体会一下什么是函数式编程和函数式的抽象过程,或许这样会更加的直观和易于理解。同时,实例本身也来源于实际的开发过程,因此更具有代表性,也便于我们进一步的了解 Functor 是如何来帮助我们解决问题的。而本文主要围绕以下这两个问题:

  • 表达式语句的函数化;
  • 条件语句的函数化;

期间,我们涉及到的类主要在以下这几个包中:

另外,关于 Functor 包中的其它一些接口的使用,我们会在下一篇里详细的介绍,而且实例本身也会涉及到函数式编程中的高阶函数的问题。

Apache Functor 实例: 表达式语句的函数化

下面我们从一个简单的例子来进一步理解 Functor 的用法。

在我们编写程序的时候,总是会遇到一些表达式,而这些表达式除了赋值语句外,更多的是对某个条件的描述,我们称这类表达式为条件判断表达式,比如我们需要遍历数组,将偶数数字打印出来,于是我们这样写判断语句

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

for( Integer number : numbers ) {
   if (number %2 == 0) {
     System.out.print(number + " ");
   }
}

让我们进一步的分析上面的这段代码,它将遍历数组和判断语句混杂在一起,然而很多时候这样的“偶数判断”是具有价值和可重用的,我们可以进一步的将它抽象出来成为某个函数,并将其命名为 isEven (由于需要返回布尔变量,我们用 Predicate 来编写)。

Predicate<Integer> isEven = new Predicate<Integer>() {
   public boolean test(Integer obj) {
     return obj % 2 == 0;
   }
};

isEven 将刚才的判断语句,变成了一个函数对象!现在我们将它应用于刚才的代码里:

for( Integer number : numbers ) {
   if (isEven.test(number)) {
     System.out.print(number + " ");
   }
}

同时我们发现“打印”这个动作也可以进一步的抽象成函数,于是我们重新定义了一个新的函数,并将其命名为 print(由于不需要返回值,我们用 Procedure 来编写):

Procedure<Integer> print = new Procedure<Integer>() {
   public void run(Integer obj) {
     System.out.print(obj + " ");
   }
};

接下来,我们将原来的代码进一步的重构:

for( Integer number : numbers ) {
   if (isEven.test(number)) {
     print.run(number);
   }
}

当然,单纯从代码的量上看,这样的写法似乎更加的“繁琐”,然而从抽象和复用的角度看,它的层次更高,代码也更加的简洁。

我们再举一个例子,遍历某个整型数组,将其元素的数字值增加一倍,并且打印出结果。如果按照以前的做法,我们是这样写的:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

for( Integer number : numbers ) {
   if (number %2 == 0) {
     number = number * 2;
     System.out.print(number + " ");
   }
}

同时我们发现,这里可以将“数字值增加一倍”这样的动作抽象成函数,于是我们重新定义一个新的函数,并将命名为 doubler(由于它需要返回值,我们用 Function 来编写):

Function<Integer, Integer> doubler = new Function<Integer, Integer>() {
   public Integer evaluate(Integer obj) {
     return obj * 2;
   }
};

我们不再需要重新发明轮子,因为我们刚才已经定义好了“偶数”判断函数(isEven)和“打印”函数 (print),现在我们只需要复用之前的代码,并用组合的方式来重新编写程序。于是刚才的代码经过重构,变成了这样:

for( Integer number : numbers ) {
   if(isEven.test(number)) {
     print.run(doubler.evaluate(number));
   }
}

那么,这样的编写代码它好在哪里呢?试想一下,我们将动作抽象成了独立的函数,并组合出所需要的代码,这样即使函数本身发生了变化(比如 print 这个函数,更改成需要 log 文件来记录),代码的主体是不需要更改的!是不是现在感觉这样的函数式编程更加的灵活呢?

Apache Functor 实例: 条件语句的函数化

好,继续我们的函数之旅吧。现在我们需要将for(...){} 这样的循环语句(循环本身也是某个动作)也抽象成函数!这里需要用到 org.apache.commons.functor.generator 下的 Generator 接口和 org.apache.commons.functor.range 包下的 Range 接口。(在下一章,我们会详细的介绍这些包里的辅助接口和工具类,这里我们只是了解如何将条件语句隐藏起来,并实现函数化的)

以下是完整的实现代码:

IntegerRange<Integer> integerRange = new IntegerRange(1, 5); 
   
Predicate<Integer> isEven = new Predicate<Integer>() {
   public boolean test(Integer obj) {
     return obj % 2 == 0;
   }
};

FilteredGenerator<Integer> filteredGenerator = 
     new FilteredGenerator<Integer>(IteratorToGeneratorAdapter.adapt(interRange), isEven);

Function<Integer, Integer> doubler = new Function<Integer, Integer>() {
   public Integer evaluate(Integer obj) {
     return obj * 2;
   }
};

Procedure<Integer> print = new Procedure<Integer>() {
   public void run(Integer obj) {
     System.out.print(obj + " ");
   }
};

CompositeProcedure<Integer> compositeProcedure =
     new CompositeProcedure<Integer>(print);

filteredGenerator.run(compositeProcedure.of(doubler));

接下来,我们一步一步的来为大家解释代码的执行过程和实现方法。

首先,用 IntegerRange 类来初始化整数列表。其实这跟使用Arrays.asList方法生成列表没有本质的区别:

IntegerRange<Integer> integerRange = new IntegerRange(1, 5);

接着,初始化 FilteredGenerator 类,并将整数列表(integerRange)和“偶数”判断函数(isEvent)作为构造函数的参数传递给它:

FilteredGenerator<Integer> filteredGenerator = 
     new FilteredGenerator<Integer>(IteratorToGeneratorAdapter.adapt(interRange), isEven);

第三步,我们需要将“打印”函数 (print) 和“双倍”函数(doubler),用 CompositeProcedure 组合在一起。

CompositeProcedure<Integer> compositeProcedure =
     new CompositeProcedure<Integer>(print);

这里需要稍微停顿一下,之前我们是如何将“打印”函数 (print) 和“双倍”函数(doubler)组装起来的?

for( Integer number : numbers ) {
   if(isEven.test(number)) {
     print.run(doubler.evaluate(number));
   }
}

对了, 就是print.run(doubler.evaluate(number))。那么,是不是可以这样理解,CompositeProcedure 类只不过是将上面的“冗长”的代码包装成了一个类而已呢?CompositeProcedure API里的解释是:

Procedure representing the composition of Functions,
“chaining”the output of one to the input of another。
 For example:new CompositeProcedure(p).of(f) runs to p.run(f.evaluate(obj)),
and new CompositeProcedure(p).of(f).of(g) runs p.run(f.evaluate(g.evaluate(obj)))

也就是说如果我们调用compositeProcedure.of(doubler)方法,它就会将“打印”函数 (print) 和“双倍”函数(doubler)组装起来形成一个完整的 Procedure 类。而根据 Procedure 接口的描述,它拥有 run 方法,并且会接受一个参数但并没有返回值。

那么,如果我们调用compositeProcedure.run(x)方法,它就会按照print.run(double.evalute(x))的方式展开,从而完成双倍数据并打印的执行过程,看来和我们当初猜测的完全一致!

好了,我们继续吧。

第四步,通过调用 FilteredGenerator 的 run 方法,来将 for 循环语句隐藏起来。

filteredGenerator.run(compositeProcedure.of(doubler));

或许你会奇怪,原来的 for 循环到底是如何被隐藏起来的?让我们一起去看看 FilteredGenerator 类中的 run 方法实现:

 public void run(Procedure<? super E> proc) {
 getWrappedGenerator().run(new ConditionalProcedure<E>(pred, proc));
 }

(注:这里的getWrappedGenerator()方法,会返回一个 InteratorToGeneratorAdapter 对象)

很明显它调用了 InteratorToGeneratorAdapter 的 run 方法,并将一个 ConditionalProcedure 对象作为其参数(注:关于 ConditionProcedure 对象的执行过程我们稍候会解释)。

我们再来看看 IteratorToGeneratorAdapter 的 run 方法(注:InteratorToGeneratorAdapter 继承自 LoopGenerator)。

   public void run(Procedure<? super E> proc) {
 while (iter.hasNext()) {
 proc.run(iter.next());
 if (isStopped()) {
 break;
 }
 }

原来 for 循环在这里被展开,并且调用刚才传递过来的 ConditionalProduce 对象的 run 方法,并且将每次迭代的对象作为其参数。

到这里,就只剩下一个疑问了,“偶数”判断函数(isEven)是如何与刚才的 compositeProcedure 组合起来的?换句话说,if(isEven.test(number)) 是如何被实现的?

ConditionalProduce API中是这样描述的:

Procedure similiar to Java's "ternary" or "conditional" operator (? :)。Given a predicate p and procedures q and r,runs if (p.test(x)) { q.run(x); } else { r.run(x); }。

而这里的 p 显然就是 isEven,而 q 就是 compositeProcedure,其执行的过程也就类似于:

if(isEven(x)){q.run(x)}

结束语

在这篇文章中,我们从一段实际的代码出发,提出问题并设法按照动作抽象出不同的函数对象,让读者一步步体会函数式编程的不同思维模式,以及这样的模式所带来的系统灵活性。同时我们进一步的引导读者将抽象层次再次提高,将 for 循环也抽象成函数对象,从而运用组合的方式完成同样的功能。这让我想起来,在设计模式的学习过程里,我们总是反复的提倡使用组合而不是继承,因为这样更能体现组件的复用和框架的灵活性。阅读 Functor 的代码,让人感觉编写程序更像是在垒积木,通过不同的函数模块,通过组合的方式将这些模块组装在一起成为一个完整的功能模块。虽然从代码量上它显得有些罗嗦,但你会感觉到它的抽象层次更高,而且由于我们抽象出了更多的函数模块,软件的复用性和可扩展性更强。

其实,Apache Functor 早在 2012 年就出现在 Apache Commons 模块中,它虽然仅仅只是在应用层面增加了对函数式编程的支持,但这种编程范式带来的却是一种思想上的震撼!这次的 Java 函数式之旅,仅仅只是带大家领略 Apache Functor 在具体的表达式语句和条件语句的函数化方面的解决方案,至于其他的特性(例如高阶函数的表达)等,希望能够在接下来的文章里和大家一起分享。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=990800
ArticleTitle=Java 的函数式之旅:使用 Apache Functor 体验函数式编程
publish-date=12012014