首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》10.4 Clojure序列

关灯直达底部

看下面这段代码中的Java迭代器。这是使用迭代器的老套路了。实际上,Java 5里的for循环在底层也会被转换成这种实现:

Collection<String> c = ...;for (Iterator&<String> it = c.iterator; it.hasNext;) {  String str = it.next;  ...}  

对于简单集合的循环处理这就够了,比如SetList。但Iterator接口只有nexthasNext方法,加上一个可选的remove方法。

1. 残缺的Java迭代器

然而Java迭代器还有缺陷。迭代器接口所提供的集合交互方法满足不了需求。用Iterator只能做两件事:

  • 查看集合中是否还有更多的元素;
  • 取出下一个元素,并把迭代器向前推进。

Iterator最主要的问题是把取得下一个元素和向前推进合在了一起(如图10-5所示)。这意味着无法先对集合中的下一个元素进行检查,然后再决定它是需要特殊处理,还是完好无损地取出去。

图10-5 Java迭代器的性质

从迭代器中取出下一元素的行为改变了它的状态。也就是说可变已经内建在Java处理集合和迭代器的方法中了,因此不可能用它构建出强健的多路解决方案。

2. Clojure的键抽象

Clojure采用了不同的方式。Clojure与Java中的集合与迭代器相对应的核心概念是序列(sequence),或者简称seq。它基本上是把两个Java类的一些特性集成到了一个概念里。这样做的动机有三个:

  • 更强健的迭代器,特别是对于多路算法而言;
  • 不可变能力,可以安全地在函数间传递序列;
  • 实现懒序列的可能性(后面还会详细讨论)。

表10-4中列出了跟序列相关的一些核心功能。这些函数都不会改变它们的参数,如果它们需要返回不同的值,那会是一个不同的序列。

表10-4 基本的序列函数

函数作用(seq <coll>)返回一个序列,作为所操作集合的“视图“(first <coll>)返回集合的第一个元素,如有必要,先在其上调用(seq) 。如果集合为nil,则返回nil(rest <coll>)返回从集合中去掉第一个元素后得到的新序列。如果集合为nil,则返回nil(seq? <o>)如果o是一个序列则返回true(也就是实现了ISeq(cons <elt> <coll>)在集合前面增加新元素,并返回由此得到的序列(conj <coll> <elt>)返回将新元素加到合适一端(向量的尾端和列表的头)的新集合(every? <pred-fn> <coll>)如果(pred-fn)对集合中的每个元素都返回逻辑真,则返回true

这里有几个例子:

1:1 user=> (rest /'(1 2 3))(2 3)1:2 user=> (first /'(1 2 3))11:3 user=> (rest [1 2 3])(2 3)1:13 user=> (seq )nil1:14 user=> (seq )nil1:15 user=> (cons 1 [2 3])(1 2 3)1:16 user=> (every? is-prime [2 3 5 7 11])true  

有一点要重点关注一下,列表是自身的序列,而向量不是。因此从理论上来说,不能在向量上调用(rest)。可实际上是可以的,因为(rest)在操作向量之前先在其上调用了(seq)。这是序列结构中普遍存在的属性:很多序列函数都会接受比序列更通用的对象,并在开始之前先调用(seq)

我们在这一节中准备探索seq的一些基本属性和用法,尤其会重点关注懒序列和变参函数。其中第一个概念”懒“,是Java中不太会涉及的编程技术1,所以对你来说它可能比较新颖。现在我们就来看一下吧。

1 用过Hibernate的人一定知道懒加载(因为它原来经常爆异常),其基本思路”延迟“跟懒是一样的。——译者注

10.4.1 懒序列

在编程语言里,懒是一个强大的概念。其基本思想是将表达式的计算推迟到需要时。体现在Clojure中就是序列可以不是完整的值列表,其中的值可以在被请求时取得(比如根据需要通过调用函数生成它们)。

在Java中,要满足这样的想法就得靠定制的List实现,而且要写大量的套路化代码才可能实现。用Clojure中的宏只要做一点儿工作就能创建出懒序列。

想一想怎么才能创建出一个懒惰的、可能包含无限数量值的序列。很明显,用函数来生成序列内的元素。这个函数应该做两件事:

  • 返回序列中的下一个元素;
  • 接受数量固定、有限的参数。

数学家会说这样一个函数定义的是递归关系,并且这样的关系用递归的方式处理再恰当不过了。

假设有一台在栈空间和其他能力上都不受限制的机器,并且可以执行两个线程:一个用来生成无限的序列,另外一个使用该序列。那我们就可以在生成线程里用递归定义懒序列,类似下面这段伪代码:

(defn infinite-seq <vec-args>(let [new-val (seq-fn <vec-args>)]    (cons new-val (infinite-seq <new-vec-args>))))  

实际上在Clojure中这是行不通的,因为(infinite-seq)上的递归会导致栈溢出。但要是加上一个结构,告诉Clojure不要疯狂递归,仅根据需要进行处理,是可以做到的。

不仅如此,你还能在一个线程内做到这一点,如下例所示。代码清单10-5中为某个数k定义了懒序列k, k+1, k+2, ...

代码清单10-5 懒序列的例子

(defn next-big-n [n] (let [new-val (+ 1 n)]  (lazy-seq  ; //lazy-seq标记     (cons new-val (next-big-n new-val))  ; //无限递归)))(defn natural-k [k]  (concat [k] (next-big-n k)))  ; //concat限制递归1:57 user=> (take 10 (natural-k 3))(3 4 5 6 7 8 9 10 11 12)  

(lazy-seq)形式是关键,它标记了发生无限递归的点,还有(concat),可以安全地处理递归。然后你就可以用(take)形式从懒序列中取出所需的元素了,这个基本上是用(next-big-n)形式定义的。

懒序列是极其强大的特性,实践会告诉你它们是Clojure军火库中的强大武器。

10.4.2 序列和变参函数

Clojure函数有一个强大的特性,它天生就具备参数数量可变的能力,有时称为函数的变元(arity)。参数数量可变的函数称为变参函数(variadic)。

代码清单10-1中讨论过的函数(const-fun1)可以作为一个简单的例子。这个函数接受一个参数并抛弃它,总是返回值1。请看传入多个参数给(const-fun1)时会发生什么:

1:32 user=> (const-fun1 2 3)java.lang.IllegalArgumentException: Wrong number of args (2) passed to:user$const-fun1 (repl-1:32)  

Clojure编译器仍然会对传给(const-fun1)的参数数量(和类型)做一些检查。对于简单地抛弃所有参数并返回一个常量值的函数来说,这似乎过于严格了。在Clojure中能接受任意数量参数的函数看起来会是什么样的呢?

代码清单10-6展示了如何实现一个这样的(const-fun1)常量函数。我们管它叫(const-fun-arity1),变元的const-fun1。这是在Clojure标准函数库中(constantly)函数的自产版。

代码清单10-6 带有变元的函数

1:28 user=> (defn const-fun-arity1   ( 1)  ;      //带不同签名的多个defn  ([x] 1)        //带不同签名的多个defn  ([x & more] 1) //带不同签名的多个defn)#/'user/const-fun-arity11:33 user=> (const-fun-arity1)11:34 user=> (const-fun-arity1 2)11:35 user=> (const-fun-arity1 2 3 4)1  

这个函数的定义不是一个参数向量后跟着函数行为的定义。而是有一系列这种组合,每个组合里都是一个参数向量(构成了这一版本函数的有效签名)和这一版本函数的实现。

这跟Java的方法重载类似。传统做法一般是定义几个特殊情况下的形式(没有参数、一个或两个参数)和最后一个参数为序列的额外形式。代码清单10-6中就是参数向量为[x & more]的那个。&符号表明这是该函数的变参版本。

序列是Clojure的创新。实际上,用Clojure编程主要就是要思考怎么用序列解决特定问题。

Clojure的另一项重要创新是Clojure和Java的集成,也就是我们下一节的主题。