65.9K
CodeProject 正在变化。 阅读更多。
Home

函数式 Java

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.11/5 (6投票s)

2009年11月13日

CPOL

8分钟阅读

viewsIcon

41524

downloadIcon

301

使用 Java 中的函子 (functor) 和对象流 (object streams) 进行函数式编程。

引言

swensen.functional 包的目的是为 Java 提供

  1. 一组通用函子,
  2. 一个统一 Java 数组和 Iterable 的不可变包装器类型,支持方法链式调用和惰性函数转换,以及
  3. 一组通用的元组 (tuple) 类型,以支持函数式编程中常见的临时数据视图。

该项目由作者构思,旨在帮助作者熟悉 Java,并受到作者使用 C# 和 F# 经验的启发。swensen.functional 中的类型并不旨在扩展 Java 语言本身,它们与核心语言特性协同工作,尤其大量使用了泛型和匿名内部类。本着这一精神,作者的意图是最大化该库在开发者日常 Java 编程实践中的实用性,并最小化他们需要学习的新工具集;我们不需要另一个动态类型 DSL 或过度设计的框架。为此,我们获得了函数式编程的相当大的表达能力和保证,但不一定有其简洁性,这在 Java 语言的限制范围内是不可能实现的。

通用函子 (Func0, Func1, Func2, ... ; Action0, Action1, Action2, ...)

ComparatorRunnable 是 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);
}

这里我们看到 T1T2R 是类型参数,允许我们封装任何具有两个(非原始类型)参数 T1T2 和一个(非原始类型)返回类型 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 还提供了一组函子,用于促进通用函子和遗留函子之间的兼容性:CallableFuncComparatorFuncRunnableAction。这些 abstract 类中的每一个都实现了它们指示的遗留函子及其通用类,并且可以直接通过匿名内部类或接受超类型现有实例的重载 static 方法 from 来实例化。可根据请求添加其他兼容函子。另一个函子 Predicate<T> 是一个 abstract 类,实现了 Func1<T,Boolean> 并公开了几个用于构建复合 Predicates 的方法。

序列 (Seq)

Seq 是一个实现 Iterable 的不可变类型,支持方法链式调用和惰性求值。Seqs 通过重载的 static 方法 of 来构建,该方法接受 Iterables 以及所有原始类型和通用可变参数数组。实际的包装数据结构不会复制到生成的 Seq 中,并且永远不会被修改:所有针对 Seqs 的操作都会生成新的 Seqs。这是通过使用匿名构造的 Iterator 按需生成 Seq 元素来实现的(这是 Seq 类中贯穿使用的技术)。

请注意,尽管 Seq 本身是不可变的,但它允许对其此保证的某些脆弱性

  1. 在构建 Seq 时,提供的 Iterator 未被复制,因此可能会被外部引用持有者修改,这是出于性能考虑的允许。
  2. 它有一个公共构造函数,唯一的目的是允许扩展(静态 of 方法是首选的构造方法),在对库使用者提供保证和对库设计者提供灵活性之间取得折衷。

总的来说,这是我们在开发不可变类型(如进一步的 Tuples)时所倾向的设计折衷。

由于 Seqs 是不可变的,其底层的 Iteratorremove 方法应始终抛出 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 大量使用了我们之前介绍的函子。它包括诸如 filtermapfoldl 等基本函数式方法,以及许多其他用于操作和生成 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 版本的新增功能,因为作者不想引入不必要的元素供开发者学习。但最终,人们意识到这些对于实现丰富的函数式表达能力至关重要,而替代方案需要一个更繁重的、不那么通用的类型集合。与 FuncXActionX 类似,我们有一组通用的 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 方法构造 Seqs 的所有方式

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,重载了 intlongfloatdoubleBigIntegerBigDecimal,包括反向范围和步长

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 标准库或您自己的库中任何使用 Iterables 的地方使用。此外,Seq 还实现了一些将 Seqs 转换为标准 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();
        }
    }
);

Seqs 可以通过连接其他 Seqs 来创建

/* 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));

Lists 和 Arrays 中典型的基于索引/长度的方法提取子序列不同,函数式数据结构通常使用 takeskip 方法的组合

/* 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);

SeqtoString 提供了合理的重载,以及结构化的、成对的 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();

Seqs 也可以方便地作为集合进行操作,尽管这些不是惰性实现的

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();

除了这里探讨的之外,还可以对 Seqs 执行许多其他方法,而使用 Seq 进行函数式编程的真正强大之处在于将这些方法串联成一系列轻量级的步骤。

单元测试

该项目代表了我首次进行单元测试的经验。从那时起,我在 Groovy on Grails 和 Java Swing 项目中通过与高度重视此实践的团队合作,扩展了我在单元和集成测试方面的经验。我发现 swensen.functional 的测试既有趣又令人满意,因为无需任何 Mock 框架或动态功能即可有效地测试这些库;由于充满了所有不可变数据结构和纯函数,因此没有需要考虑的内部依赖项。确实,函数式库中的单元测试只是对可以数学证明的算法实现的验证。给定一组输入,输出总是相同的。这在重构和实现调整中建立了极大的信心,这确实是单元测试的总体目标之一,但在我看来,在充满突变并且因此依赖 Mock 框架的测试的项目中,其稳固性从未如此高。

挑战

初始设计阶段相当紧张。特别是,选择函子的最佳表示法是一个早期挑战。起初,我尝试了使用反射调用标准方法的动态函子。但泛型的不足(无法运行时反射参数化类型)、复杂性(例如,解析重载方法),以及意识到基于反射的方法会让许多珍视编译时类型检查的用户感到不满,最终使我决定采用简单、通用、强类型、熟悉的接口方法,这些接口将实例化为匿名类。

可能最大的、持续的挑战是 Iterator 接口的笨拙,它是所有惰性求值 Seq 方法的核心。特别是 hasNext(),它

  1. 执行一项潜在的昂贵操作,
  2. 可能会被重复调用。

因此,某些算法必须特别注意在调用 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日
  • 为多个方法添加了 null 参数检查。
  • Seq.anySeq.all 改为接受 Func1 而不是 Predicate
  • ComparatorFunc2ComparatorFunc 重载了 Seq.min/max/sort
  • 修复了 Seq.isEmpty()
1.02 2010年3月7日
  • Seq.min/max/sort 绕过了 Mac OSX Java 6 编译器中的一个 bug。
  • 修复了一些 JavaDoc 问题(通过从 Eclipse 切换到 IntelliJ IDEA 得到启发)。
  • 创建了 test.swensen.functional.SeqTest JUnit 4 测试类,并为 Seq 创建了基本测试用例。
1.03 2010年5月22日
  • 修复了 Seq.foldl 未尊重成对结合律的 bug。
2.0,主要版本发布 2010年12月5日
  • Seq.eager 重命名为 Seq.force
  • Seq.elementAt 重命名为 Seq.ith
  • 添加了 Seq.nth
  • Seq.skipSeq.take 替换了 Seq.subSequence
  • 添加了带步长的 Seq.range 重载。
  • LongFloatDoubleBigIntegerBigDecimal 添加了 Seq.range 重载。
  • 修复了 Seq.filter 中潜在的堆栈溢出 bug。
  • 添加了 Seq.takeWhileSeq.skipWhile
  • 移除了接受终止谓词的 generate 重载:现在使用 generate().takeWhile()
  • 添加了 truncate
  • 添加了 Tuple 类型。
  • 添加了 Seq.groupBy 和支持的 GroupingGroupingSeq 类型(分别扩展 TupleSeq 类型)。
  • 添加了静态 Seq.concat 方法。
  • 添加了 Seq.partition 和支持的 Partition 类型(扩展 Tuple)。
  • 添加了 Seq.iteriSeq.itern
  • 将所有 NullPointerException 的使用更改为 IllegalArgumentException
  • 将目标 JRE 从 1.6 更改为 1.5 update 22。
  • 添加了 Seq.shuffle
  • 添加了 Seq.unboundedRange 重载。
  • 添加了 Seq.zipSeq.zip
© . All rights reserved.