看下面这段代码中的Java迭代器。这是使用迭代器的老套路了。实际上,Java 5里的for
循环在底层也会被转换成这种实现:
Collection<String> c = ...;for (Iterator&<String> it = c.iterator; it.hasNext;) { String str = it.next; ...}
对于简单集合的循环处理这就够了,比如Set
或List
。但Iterator
接口只有next
和hasNext
方法,加上一个可选的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的集成,也就是我们下一节的主题。