函数式 Java






4.11/5 (6投票s)
使用 Java 中的函子 (functor) 和对象流 (object streams) 进行函数式编程。
引言
swensen.functional
包的目的是为 Java 提供
- 一组通用函子,
- 一个统一 Java 数组和 Iterable 的不可变包装器类型,支持方法链式调用和惰性函数转换,以及
- 一组通用的元组 (tuple) 类型,以支持函数式编程中常见的临时数据视图。
该项目由作者构思,旨在帮助作者熟悉 Java,并受到作者使用 C# 和 F# 经验的启发。swensen.functional
中的类型并不旨在扩展 Java 语言本身,它们与核心语言特性协同工作,尤其大量使用了泛型和匿名内部类。本着这一精神,作者的意图是最大化该库在开发者日常 Java 编程实践中的实用性,并最小化他们需要学习的新工具集;我们不需要另一个动态类型 DSL 或过度设计的框架。为此,我们获得了函数式编程的相当大的表达能力和保证,但不一定有其简洁性,这在 Java 语言的限制范围内是不可能实现的。
通用函子 (Func0, Func1, Func2, ... ; Action0, Action1, Action2, ...)
Comparator
和 Runnable
是 Java 中函子的著名例子。它们是指定单个方法的接口,并且通常通过匿名内部类创建。借助泛型,我们可以放弃临时函子,将自己限制在两种类型中:FuncX
(非 void 返回类型)和 ActionX
(void 返回类型),其中 X 是函子封装的参数数量(在 swensen.functional
中实现到 7 个)。例如,我们来看 Func2
package swensen.functional;
/**
* @author Stephen Swensen
* A generic functor for non-Void methods with two parameters.
* @param <T1> the type of the first parameter of call
* @param <T2> the type of the second parameter of call
* @param <R> the return type of call
*/
public interface Func2 <T1,T2,R> {
/**
* Invoke this functor synchronously.
* @param t1 the first parameter
* @param t2 the second parameter
* @return the return value
*/
public R call(T1 t1, T2 t2);
}
这里我们看到 T1
、T2
和 R
是类型参数,允许我们封装任何具有两个(非原始类型)参数 T1
、T2
和一个(非原始类型)返回类型 R
的方法。我们可以这样使用它
Func2<String,String,Integer> func = new Func2<String,String,Integer>() {
public Integer call(String t1, String t2) {
t1 = (t1 == null) ? "" : t1.trim();
t2 = (t2 == null) ? "" : t2.trim();
return t1.length() - t2.length();
}
};
Integer result = func.call("hello world ", " hello world"); //result == 0
swensen.functional
还提供了一组函子,用于促进通用函子和遗留函子之间的兼容性:CallableFunc
、ComparatorFunc
和 RunnableAction
。这些 abstract
类中的每一个都实现了它们指示的遗留函子及其通用类,并且可以直接通过匿名内部类或接受超类型现有实例的重载 static
方法 from
来实例化。可根据请求添加其他兼容函子。另一个函子 Predicate<T>
是一个 abstract
类,实现了 Func1<T,Boolean>
并公开了几个用于构建复合 Predicate
s 的方法。
序列 (Seq)
Seq
是一个实现 Iterable
的不可变类型,支持方法链式调用和惰性求值。Seq
s 通过重载的 static
方法 of
来构建,该方法接受 Iterable
s 以及所有原始类型和通用可变参数数组。实际的包装数据结构不会复制到生成的 Seq
中,并且永远不会被修改:所有针对 Seq
s 的操作都会生成新的 Seq
s。这是通过使用匿名构造的 Iterator
按需生成 Seq
元素来实现的(这是 Seq
类中贯穿使用的技术)。
请注意,尽管 Seq
本身是不可变的,但它允许对其此保证的某些脆弱性
- 在构建
Seq
时,提供的Iterator
未被复制,因此可能会被外部引用持有者修改,这是出于性能考虑的允许。 - 它有一个公共构造函数,唯一的目的是允许扩展(静态
of
方法是首选的构造方法),在对库使用者提供保证和对库设计者提供灵活性之间取得折衷。
总的来说,这是我们在开发不可变类型(如进一步的 Tuples)时所倾向的设计折衷。
由于 Seq
s 是不可变的,其底层的 Iterator
的 remove
方法应始终抛出 UnsupportedOperationException
。因此,我们创建了一个名为 ReadonlyIterator
的抽象类,它扩展了 Iterator
以实现此目的
public abstract class ReadonlyIterator<E> implements Iterator<E> {
public void remove() {
throw new UnsupportedOperationException();
}
}
我们演示了在从通用可变参数数组构建 Seq
时如何使用它
/**
* Construct a sequence from a generic array.
* Lazy Iterable wrapper around arr, no new memory is allocated.
* Outside structural mutation to arr compromises the immutability of this Seq.
* Note that since we override Seq.of for each and every primitive array type, we will
* never get surprising behavior like that seen with Arrays.asList.
* @param <E> element type
* @param arr an array, may be null
* @return a new Seq constructed from arr
* (or Seq.EMPTY if arr is null or length 0)
*/
public static <E> Seq<E> of(final E...arr)
{
if(arr == null || arr.length == 0)
return Seq.getEmpty();
return Seq.of(new Iterable<E>(){
public Iterator<E> iterator() {
return new ReadonlyIterator<E>(){
int cur = 0;
public boolean hasNext() {
return cur < arr.length;
}
public E next() {
if(cur < arr.length)
return arr[cur++];
else
throw new NoSuchElementException();
}
};
}
});
}
Seq
大量使用了我们之前介绍的函子。它包括诸如 filter
、map
和 foldl
等基本函数式方法,以及许多其他用于操作和生成 Seq
对象流的有用方法。下面是我们享受的编程形式和风格的样本
/* elements generated on demand, no memory allocated
up front except for Seq object itself. */
Seq<Integer> range1 = Seq.range(1, 10);
/* range1 is neither mutated nor traversed at this time. */
Seq<Integer> range2 = range1.append(34, 36, 38, 39);
Func1<Integer,Integer> minus3 = new Func1<Integer,Integer>(){
public Integer call(Integer t1) {
return t1 - 3;
}
};
/* could alternatively use Predicate<Integer> */
Func1<Integer,Boolean> isPositiveOdd = new Func1<Integer,Boolean>(){
public Boolean call(Integer t1) {
return (t1 % 2) == 1;
}
};
/* note the use of method chaining
{1,2,3,4,5,6,7,8,9,10,34,36,38,39} ->
{-2,-1,0,1,2,3,4,5,6,7,31,33,35,36} -> {1,3,5,7,31,33,35} */
Seq<Integer> result = range2.map(minus3).filter(isPositiveOdd);
Action1<Object> println = new Action1<Object>() {
public void call(Object obj){
System.out.println(obj);
}
};
/* apply the println Action to each element immediately. */
result.iter(println);
Func2<Integer,Integer,Integer> sum = new Func2<Integer,Integer,Integer>(){
public Integer call(Integer t1,Integer t2) {
return t1 + t2;
}
};
/* 115 */
Integer resultSum = result.foldl(0, sum);
元组 (Tuple1, Tuple2, Tuple3, ...)
决定包含元组实现是一个艰难的决定,也是 2.0 版本的新增功能,因为作者不想引入不必要的元素供开发者学习。但最终,人们意识到这些对于实现丰富的函数式表达能力至关重要,而替代方案需要一个更繁重的、不那么通用的类型集合。与 FuncX
和 ActionX
类似,我们有一组通用的 TupleX
实现(当然是不可变的),其中 X 是表示的字段数,从 1 到 7(任意)。此外,我们有一个名为 Tuples
的类,其中包含用于构建各种元组的静态方法,这使我们能够获得比 TupleX
构造函数更好的类型推断的好处,以及一个用于所有构造需求的单一重载方法 create
。让我们看看 Tuple3
的实现;注意我们对 toString()
、equals()
和 hashCode()
的实现
public class Tuple3<T1,T2,T3> {
//note that we can use public fields instead
//of getters since they are marked final
public final T1 t1;
public final T2 t2;
public final T3 t3;
public Tuple3(T1 t1, T2 t2, T3 t3) {
this.t1 = t1;
this.t2 = t2;
this.t3 = t3;
}
public static <T1,T2,T3> Tuple3<T1,T2,T3> create(T1 t1, T2 t2, T3 t3)
{
return new Tuple3<T1,T2,T3>(t1,t2,t3);
}
public String toString()
{
return String.format("(%s, %s, %s)", t1, t2, t3);
}
public boolean equals(Object other) {
if (this == other)
return true;
if (!(other instanceof Tuple3<?,?,?>))
return false;
Tuple3<T1,T2,T3> rhs = (Tuple3<T1,T2,T3>) other;
if (!(this.t1==null ? rhs.t1==null : this.t1.equals(rhs.t1)))
return false;
if (!(this.t2==null ? rhs.t2==null : this.t2.equals(rhs.t2)))
return false;
if (!(this.t3==null ? rhs.t3==null : this.t3.equals(rhs.t3)))
return false;
return true;
}
public int getHashCode() {
int hash = 1;
hash *= 31 + (t1 == null ? 0 : t1.hashCode());
hash *= 31 + (t2 == null ? 0 : t2.hashCode());
hash *= 31 + (t3 == null ? 0 : t3.hashCode());
return hash;
}
}
我们可以这样使用它
Tuple3<Integer, String, Seq<Double>> tuple3 =
Tuples.create(5, "hello", Seq.of(0.5, 1.2, 5.3));
int t1 = tuple3.t1;
String t2 = tuple3.t2;
Seq<Double> t3 = tuple3.t3;
//(5, hello, {0.5, 1.2, 5.3})
System.out.println(tuple3.toString());
一览特性
以下是本库中一些功能的有限参考
以下示例展示了使用 of
方法构造 Seq
s 的所有方式
Seq<Integer> a = Seq.of(1,2,3,4); //generic vararg array
Seq<Integer> b = Seq.of(new Integer[] {1,2,3,4}); //generic array
Seq<Integer> c = Seq.of(new int[] {1,2,3,4}); //primitive array
Seq<Integer> d = Seq.of(Arrays.asList(1,2,3,4)); //any Iterable
Seq
还提供了丰富的有界和无界范围 Seq
,重载了 int
、long
、float
、double
、BigInteger
和 BigDecimal
,包括反向范围和步长
Seq.range(-2, 2); // -2, -1, 0, 1, 2
Seq.range(2L, -2L); // 2L, 1L, 0L, -1L, -2L
Seq.range(BigInteger.valueOf(-4),
BigInteger.valueOf(4),
BigInteger.valueOf(2)); // -4, -2, 0, 2, 4 (all BigIntegers)
Seq.unboundedRange(2.0, .5); // 2.0, 2.5, 3.0, 3.5, 4.0, ...
我们还可以使用 Seq.generate
生成无限序列,它接受一个生产者函子作为参数。这是一个使用 Seq.generate
生成无限随机数序列的简单示例
final Random r = new Random();
Seq.generate(new Func0<Integer>() {
public Integer call() {
return r.nextInt();
}
});
由于 Seq
实现 Iterable
,它可以在 Java 标准库或您自己的库中任何使用 Iterable
s 的地方使用。此外,Seq
还实现了一些将 Seq
s 转换为标准 Java 集合类型的方法
List<Integer> list = Seq.of(1,1,2,2,3,3).toArrayList();
Integer[] array = Seq.of(1,1,2,2,3,3).toArray(Integer.class);
/* [1,2,3] */
Set<Integer> set = Seq.of(1,1,2,2,3,3).toHashSet();
/* [1L : "1", 2L : "2", 3L : "3"] */
Map<Long, String> map = Seq.of(1,1,2,2,3,3).toHashMap(
new Func1<Integer, Long>() {
public Long call(Integer integer) {
return integer.longValue();
}
},
new Func1<Integer, String>() {
public String call(Integer integer) {
return integer.toString();
}
}
);
Seq
s 可以通过连接其他 Seq
s 来创建
/* 1,2,3,5,6 */
Seq.of(1,2,3).append(4,5,6);
/* 4,5,6,1,2,3 */
Seq.of(1,2,3).prepend(4,5,6);
/* 1,2,3,4,5,6,7,8,9 */
Seq.concat(Seq.of(null, Seq.of(1,2,3), Seq.<Integer>getEmpty(),
Seq.of(4,5,6), null, Arrays.asList(7,8,9), null));
与 List
s 和 Array
s 中典型的基于索引/长度的方法提取子序列不同,函数式数据结构通常使用 take
和 skip
方法的组合
/* 0,1,2 */
Seq.of(0,1,2,3,4,5,6,7,8).take(3)
/* 3,4,5,6,7,8 */
Seq.of(0,1,2,3,4,5,6,7,8).skip(3)
/* 3,4,5 */
Seq.of(0,1,2,3,4,5,6,7,8).skip(3).take(3);
Seq
为 toString
提供了合理的重载,以及结构化的、成对的 equals
/* "{1, 2, null, 4, 5}" */
Seq.of(1,2,null,4,5).toString();
/* true */
Seq.of(1,2,3,4).equals(Seq.of(new int[]{1,2,3,4}));
Seq
提供了多种查询自身的方法,包括聚合函数
Seq<Integer> s = Seq.of(4,3,2,1);
/* true */
s.skip(4).isEmpty();
/* 4 */
s.count();
/* the following three methods have overloads for Comparator,
Func2, and ComparatorFunc as well */
/* 1,2,3,4 */
s.sort();
/* 4 */
s.max();
/* 1 */
s.min();
Seq
s 也可以方便地作为集合进行操作,尽管这些不是惰性实现的
Seq<Integer> s1 = Seq.of(0,0,1,1,2,2);
Seq<Integer> s2 = Seq.of(1,1,2,2,3,3);
/* 0,1,2,3 */
s1.union(s2);
/* 1,2 */
s1.intersect(s2);
/* 0,1,2 */
s1.distinct();
除了这里探讨的之外,还可以对 Seq
s 执行许多其他方法,而使用 Seq
进行函数式编程的真正强大之处在于将这些方法串联成一系列轻量级的步骤。
单元测试
该项目代表了我首次进行单元测试的经验。从那时起,我在 Groovy on Grails 和 Java Swing 项目中通过与高度重视此实践的团队合作,扩展了我在单元和集成测试方面的经验。我发现 swensen.functional
的测试既有趣又令人满意,因为无需任何 Mock 框架或动态功能即可有效地测试这些库;由于充满了所有不可变数据结构和纯函数,因此没有需要考虑的内部依赖项。确实,函数式库中的单元测试只是对可以数学证明的算法实现的验证。给定一组输入,输出总是相同的。这在重构和实现调整中建立了极大的信心,这确实是单元测试的总体目标之一,但在我看来,在充满突变并且因此依赖 Mock 框架的测试的项目中,其稳固性从未如此高。
挑战
初始设计阶段相当紧张。特别是,选择函子的最佳表示法是一个早期挑战。起初,我尝试了使用反射调用标准方法的动态函子。但泛型的不足(无法运行时反射参数化类型)、复杂性(例如,解析重载方法),以及意识到基于反射的方法会让许多珍视编译时类型检查的用户感到不满,最终使我决定采用简单、通用、强类型、熟悉的接口方法,这些接口将实例化为匿名类。
可能最大的、持续的挑战是 Iterator
接口的笨拙,它是所有惰性求值 Seq
方法的核心。特别是 hasNext()
,它
- 执行一项潜在的昂贵操作,
- 可能会被重复调用。
因此,某些算法必须特别注意在调用 hasNext()
时实际检索并存储下一个值。以 Seq.filter()
为例,它在概念上非常直接,但用 Java 的主要实现机制来实现却出奇地困难
public Seq<E> filter(final Func1<? super E,Boolean> predicate)
{
if(predicate == null)
throw new IllegalArgumentException("predicate cannot be null");
if(this == EMPTY)
return Seq.getEmpty();
final Iterable<E> that = this;
return Seq.of(new Iterable<E>(){
public Iterator<E> iterator() {
return new ReadonlyIterator<E>(){
Iterator<E> thatIterator = that.iterator();
boolean endReached = false;
boolean curSet = false;
E cur = null;
public boolean hasNext() {
if(endReached)
return false;
else if(curSet)
return true;
while(thatIterator.hasNext()) {
cur = thatIterator.next();
if(predicate.call(cur)) {
curSet = true;
return true;
}
}
return !(endReached = true);
}
public E next() {
if(hasNext()) {
curSet = false;
return cur;
}
else
throw new NoSuchElementException();
}
};
}
});
}
最后的 remarks
本文顶部的“下载源代码”链接是一个 zip 文件,其中包含 swensen.functional package
源代码(已针对 Sun JRE 版本 1.5 update 22 for Win32 进行测试)、JavaDocs 和 JUnit 4 测试。我欢迎任何建议或 bug 报告。
历史
版本 | 日期 | 描述 |
1.00 | 2009年11月11日 |
|
1.01 | 2009年11月14日 |
|
1.02 | 2010年3月7日 |
|
1.03 | 2010年5月22日 |
|
2.0,主要版本发布 | 2010年12月5日 |
|