内容


Java 的函数式之旅,第 2 部分:深度体验 Apache Functor 的函数式编程

Comments

简介

令人欣慰的是,Java 8 的函数式接口定义基本保持了与 Apache Functor 的一致。也就是说,我们在学习 Apache Functor 的一些概念和方法,也可以用到 Java 8 上!对于那些想要了解 Java 8 新特性以及函数式编程语言的读者,同样可以先学习使用 Apache Functor,逐步了解其整体结构,掌握其使用方法,为进一步的学习函数式编程语言打下基础。

上一篇文章里我们介绍了什么是函数式编程,以及编程的基本类型:命令式编程与函数式编程,围绕着这些概念我们进一步的对比了 Java 语言与函数式编程的关系,以及 Apache Functor 的一些基本概念和应用实例(表达式语句与条件语句的函数化方面的应用)。希望读者能够对函数式编程有一个大致的了解,并能够灵活的使用 Apache Functor 类库去解决实际开发中所遇到的问题,在实践中了解和体会函数式编程的优点以及特色。

在这篇文章里,我们将围绕 Apache Functor 的一些重要的接口,通过实例的形式展示这些接口的用法。希望读者可以将这些内容与我们之前介绍的 Apache Functor 的基本概念联系起来,从而全面地了解 Apache Functor 的整体结构,为进一步的学习函数式编程提供帮助。

具体来说,我们将围绕以下这些内容,展开讨论

  • Apache Functor 的 Generator 接口
  • Apache Functor 的 Range 接口
  • Apache Functor 的 Aggregator 接口
  • Java8 的函数式接口 (functional interface)

需要注意的是,Java 并不是纯粹的函数式语言,它更多的是一种混合式编程语言。而这样的应用也只能看作是 Java 对函数式编程的某种尝试,对于需要进一步了解函数式语言的读者,可以参看 Scala 语言,而如果您希望了解和运用纯粹的函数式语言,可以参看 Erlang, Haskell 与 Lisp 语言。

Apache Functor 的 Generator 接口

在 Apache Functor 中提供了一些辅助类,这些类用来最大限度的减少用户的代码数量。我们习惯于将 org.apache.commons.functor.generator 包中的类称为 Generators。下面,我们通过两个具体的实例,来进一步了解 Generator 接口的用法。

实例一:如何使用 Generator 生成偶数序列

思路:首先生成相应区间的整数序列,再根据执行(Predicate)规则过滤整数序列,从而得到我们希望的结果。

下面是具体的实现逻辑

清单 1. 使用 Generator 生成偶数序列
Generator<Integer> wrappedGenerator =
	IteratorToGeneratorAdapter.adapt(new IntegerRange(1, 4));
	
Predicate<Integer> isEven = new Predicate<Integer>() {
	public boolean test(Integer obj) {
		return obj % 2 == 0;
	}
};

解释:我们通过 IteratorToGeneratorAdapter.adapt(new IntegerRange(1, 4)) 的方法创建了"某个范围内"(这里是 [1, 4),默认是左闭合区间)的整数类型的 Generator。偶数(isEven)被视为是 Functor 类型,需要实例化成 Predicate 对象。

现在整数序列与 Functor 都有了,下面我们来看如何将两者结合起来,生成我们需要的结果。

清单 2. 使用 Generator 生成偶数序列
FilteredGenerator<Integer> filteredGenerator =
	new FilteredGenerator<Integer>(wrappedGenerator, isEven);

final StringBuilder result = new StringBuilder();
filteredGenerator.run(new Procedure<Integer>() {
	public void run(Integer obj) {
			result.append(obj);
	}
});

解释:在这里,我们通过调用了 FilteredGenerator 的 run 方法,来获得我们想要的偶数序列。不要被这里的 Procedure 迷惑,它只是匿名内部类,用来将结果 append 到 StringBuilder 上(因为是内部类,所以 result 必须用 final 关键字修饰)。

可能有读者会感到迷惑,这是怎么做到的?其实秘密就藏在 FilteredGenerator 的 run 方法里

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

当 run 方法被调用的时候,它首先将 Predicate 与 Procedure 用 ConditionalUnaryProcedure 类组合起来(ConditionalUnaryProcedure 的用法可以查看上一篇文章),并传递给 wrappedGenerator 的 run 方法执行。(有兴趣的读者可以查看 IteratorToGeneratorAdapter 的源代码,了解它的执行过程)

实例二:Generator 的默认实现 - BaseGenerator

其实,除了用实例一中的 IteratorToGeneratorAdapter 类来生成 Generator 外,我们也可以通过 BaseGenerator 类来得到。注意:由于 BaseGenerator 是抽象类,需要我们实现它的抽象方法 - run。

清单 3. Generator 的默认实现
Generator<Integer> simpleGenerator = new BaseGenerator<Integer>() {
	public void run(Procedure<? super Integer> proc) {
		for (int i = 0; i < 5; i++) {
			proc.run(Integer.valueOf(i));
		}
	}
};

解释:在这里我们使用 for 循环,并将每次循环的 i 值作为参数传递给 Procedure。这样做的目的是将数据的生成和处理过程分离开来,Generator 只是负责生成数据,至于数据的处理它委托给 Procedure 来完成,这样就可以为日后可能会出现的新的数据处理方式预留了接口,提高了系统的可扩展性。

实例三:根据 Generator 生成相应的 Collections

我们注意到 Generator 接口还有 to 和 toCollection 方法,这两个方法怎么用呢?来看看下面的程序(这里用 Junit 断言的目的是可以查看到每次的执行结果)

清单 5. 根据 Generator 生成相应的 Collections
Collection<Integer> col =
	simpleGenerator.to(CollectionTransformer.<Integer> toCollection());
assertEquals("[0, 1, 2, 3, 4]", col.toString());

Collection<Integer> fillThis = new LinkedList<Integer>();
col = simpleGenerator.to(new CollectionTransformer<Integer,Collection<Integer>>(fillThis));
assertSame(fillThis, col);
assertEquals("[0, 1, 2, 3, 4]", col.toString());

col = (Collection<Integer>) simpleGenerator.toCollection();
assertEquals("[0, 1, 2, 3, 4]", col.toString());
assertEquals("[0, 1, 2, 3, 4]", col.toString());

fillThis = new LinkedList<Integer>();
col = (Collection<Integer>) simpleGenerator.to(fillThis);
assertSame(fillThis, col);
assertEquals("[0, 1, 2, 3, 4]", col.toString());

关于 CollectionTransformer 的 evaluate 方法定义,如下

public C evaluate(Generator<? extends E> generator) {
	generator.run(new Procedure<E>() {
			public void run(E obj) {
					toFill.add(obj);
			}
	});
	return toFill;
}

解释:借助于 CollectionTransformer 的 evaluate 方法实现,我们可以很轻松的调用 Generator 的 to 以及 toCollection 方法获取到相应的 Collections。

Apache Functor 的 Range 接口

在上一节,我们已经用到了 Range 接口- IntegerRange,来生成特定范围内的整数序列。只是那一节我们的重点是 Generator,并没有过多的解释 Range 接口的定义与用法。

Range 顾名思义就是用来生成"连续"的元素,比较类似于 Collections,具体的可以查看 com.apache.commons.functor.range。

下面我们还是通过实例的方式,来展示 Ranges 的具体用法,需要注意的是,在默认的情况下 Range 是左闭合区间。

清单 6. Apache Functor 的 Range 接口
// [0, 10), 1 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
IntegerRange range = new IntegerRange(0, 10);
	
//指定 Step 的情况下
// [0, 10), 2 = 0, 2, 4, 6, 8
IntegerRange range = new IntegerRange(0, 10, 2);
	
//还可以指定区间的左右闭合状态
// (0, 10], 2 = 2, 4, 6, 8, 10
IntegerRange range = new IntegerRange(0, BoundType.OPEN, 10, BoundType.CLOSED, 2);

解释:在新的版本中,所有实现 Range 接口的类都已经被放置在 com.apache.commons.functor.range 包下,并且不再继承于 Generator,这一点在 Functor 的文档中并没有更新说明。

如果,需要将 Range 转换成 Generator,可以调用 IteratorToGeneratorAdapter 的静态方法来实现

清单 7. Apache Functor 的 Ranges 包
// [0, 10), 1 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
IteratorToGeneratorAdapter<Integer> range =
	IteratorToGeneratorAdapter.adapt(new IntegerRange(0, 10));
	
Procedure<Integer> printProcedure = new Procedure<Integer>(){
	public void run(Integer obj) {
		System.out.print(obj + " ");
	}
};
range.run(printProcedure);

Apache Functor 的 Aggregator 接口

Apache Functor 除了能够辅助数据的生成,还提供了数据的聚合功能。Aggregator 接口就是用来聚合数据的,而且在 Aggregator 中还提供了两种数据存储(Storage)的方式。一种是 List-backed store,另一种称为 no-store.(详细介绍参看 https://commons.apache.org/proper/commons-functor/aggregator.html)。

这里需要注意的是 AbstractListBackedAggregator 与 AbstractNoStoreAggregator,都继承自 AbstractTimedAggregator。以下是 AbstractTimedAggregator 的定义:

An aggregator which automatically resets the aggregated data at regular intervals and sends a notification when it is about to do so, so listeners can decide to gather the information before it is being reset (and log it etc).

AbstractTimedAggregator 会每隔一段时间,自动的执行数据聚合动作,并在每次聚合动作执行前发送相关的消息。这里 AbstractTimedAggregator 使用事件/监听器模式来完成消息的发送与接收。

当然,数据聚合离不开数据与聚合动作。Apache Functor 就是将动作从场景中抽取出来,形成 Java 风格的函数式编程范式。在这里也是一样,数据的聚合被包装成不同形式的类,放置在 org.apache.commons.functor.aggregator.functions 包中,需要注意的是这里的 Functor 主要是针对 List 的操作:

  • DoubleMaxAggregatorBinaryFunction
  • DoubleMaxAggregatorFunction
  • DoubleMeanValueAggregatorFunction
  • DoubleMedianValueAggregatorFunction
  • DoublePercentileAggregatorFunction
  • DoubleSumAggregatorBinaryFunction
  • DoubleSumAggregatorFunction
  • IntegerCountAggregatorBinaryFunction
  • IntegerMaxAggregatorBinaryFunction
  • IntegerMaxAggregatorFunction
  • IntegerMeanValueAggregatorFunction
  • IntegerMedianValueAggregatorFunction
  • IntegerPercentileAggregatorFunction
  • IntegerSumAggregatorBinaryFunction
  • IntegerSumAggregatorFunction

实例一:如何根据 Aggregator 接口计算 List 的平均值

参考上面的列表,我们发现 DoubleMeanValueAggregatorFunction 就是用来计算 List 平均值的 Functor,我们来看看它是如何与 Aggregator 一起共同工作的:

清单 8. 根据 Aggregator 接口计算 List 的平均值
AbstractTimedAggregator<Double> aggregator =
	new ArrayListBackedAggregator<Double>(new DoubleMeanValueAggregatorFunction(), 2000L);
aggregator.add(1.0);
aggregator.add(2.0);
		
Double result = aggregator.evaluate();
assertEquals(1.5, result);
assertEquals(2, aggregator.getDataSize());

解释:这里我们将 DoubleMeanValueAggregatorFunction 以参数的形式传递给 ArrayListBackedAggregator 并设置 time interval 的值为 2 秒(2000L 表示 2000 毫秒,类型为 long 型),调用 Aggregator 的 evaluate 方法就会得到对应的平均值。

当然,我们还可以使用监听器的模式获取结果:

清单 9. 根据 Aggregator 接口计算 List 的平均值
public void testTimerListener() throws Exception{
	AbstractTimedAggregator<Double> aggregator =
	new ArrayListBackedAggregator<Double>(new DoubleMeanValueAggregatorFunction(), 2000L);
	aggregator.addTimerListener(new TimeListener());
		
	aggregator.add(2.0);
	aggregator.add(3.0);
	aggregator.add(4.0);
		
	Double result = aggregator.evaluate();
	assertEquals(3.0, result);
	Thread.sleep(5000);
}
	
public class TimeListener implements TimedAggregatorListener<Double> {
public void onTimer(AbstractTimedAggregator<Double> aggregator, Double evaluation) {
	System.out.println("get the result on time listener, " + evaluation);
	assertEquals(3.0, evaluation);
	aggregator.stop();
}
}

解释:在 testTimerListene() 测试案例里,通过调用 Thread.sleep(5000) 让执行线程暂定 5 秒,是希望在 onTimer 方法中可以收到相应的消息,执行完毕后调用 aggregator.stop() 方法,关闭消息发送。

Java8 的函数式接口 (functional interface)

Java SE 8 引入了 Lambda 表达式,这是该版本发布的最重要新特性(具体的 Lambda 表达式的相关内容,可以参考 JSR 335 定义)。

而要理解 Lambda 表达式,需要先了解 Java 对于函数式接口(functional interface)的定义是什么?简单的说,函数式接口就是只包含一个抽象方法的接口。

比如 Java 标准库中的 java.lang.Runnable 和 java.util.Comparator 都是典型的函数式接口。对于函数式接口,除了可以使用 Java 中标准的方法来创建实现对象之外,还可以使用 Lambda 表达式来创建实现对象。这可以在很大程度上简化代码的实现。在使用 Lambda 表达式时,只需要提供形式参数和方法体。由于函数式接口只有一个抽象方法,所以通过 Lambda 表达式声明的方法体就肯定是这个唯一的抽象方法的实现,而且形式参数的类型可以根据方法的类型声明进行自动推断。

以 Runnable 接口为例来进行说明,传统的创建一个线程并运行的方式如下所示:

清单 10. Runnable 接口定义
public void runThread() {
 new Thread(new Runnable() {
 public void run() {
 System.out.println("Run!");
 }
 }).start();
}

解释:在上面的代码中,首先需要创建一个匿名内部类实现 Runnable 接口,还需要实现接口中的 run 方法。如果使用 Lambda 表达式来完成同样的功能,得到的代码非常简洁,如下面所示:

清单 11. Runnable 接口的函数式定义
public void runThreadUseLambda() {
 new Thread(() -> {
 System.out.println("Run!");
 }).start();
}

解释:相对于传统的方式,Lambda 表达式在两个方面进行了简化,首先是 Runnable 接口的声明,这可以通过对上下文环境进行推断来得出;其次是对 run 方法的实现,因为函数式接口中只包含一个需要实现的方法。

JDK 8 之前已有的函数式接口

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

与 Apache Functor 中的概念类似,Java 在新版本中也定义了相应的函数式接口以及基本数据类型的子接口,它们都被放在了 java.util.function 包下

表 1. 类名及含义解析
类名含义
Predicate 传入一个参数,返回一个 boolean 结果,方法为 boolean test(T t)
Consumer 传入一个参数,无返回值,方法为 void accept(T t)
Function 传入一个参数,返回一个结果,方法为 R apply(T t)
Supplier 无参数传入,返回一个结果,方法为 T get()
UnaryOperator 一元操作符,继承 Function,传入参数的类型和返回类型相同
BinaryOperator 二元操作符,传入的两个参数的类型和返回类型相同,继承 BiFunction

总结

Apache Functor 的出现是 Java 语言在函数式编程上的一种尝试。程序就是将某种形式的数据输入转变成其他形式的输出的过程,而这其中就牵扯到数据与数据的处理。用什么样的编程语言其实都可以解决问题本身,所以编程语言的选择只是一种手段或者方法,既然是方法,那么讨论什么方法更加优秀就显得没有什么意义,而应该说什么方法更适合解决问题本身,这才是关键!

Java 是命令式编程语言,简单而易于编写。但往往它将数据与处理过程混在一起,不利于程序的扩展。数据的处理过程其实就是函数,如何将这样的过程抽取出来,这其实就是 Apache Functor 所关注的。但另一方面,我们要知道使用 Apache Functor,只是 Java 的一种函数式编程的尝试,即使最新版本的 Lambda 也只是接口的语法糖而已,根本谈不上函数式编程。

即使这样,借助于 Apache Functor,Java 的开发者们依然可以领略到函数式编程的魅力。尝试将数据的处理过程抽象出来,也为我们的编程提供了另外的思路,未尝不是一件好事。虽然 Apache Functor 项目已经不再更新了,但令人欣慰的是在 Java 8 中我们看到了它的重生。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology,
ArticleID=1035790
ArticleTitle=Java 的函数式之旅,第 2 部分:深度体验 Apache Functor 的函数式编程
publish-date=08102016