Clojure-JVM上的函数式编程语言(2) 集合 作者: R. Mark Volkmann
?原帖地址:http://java.ociweb.com/mark/clojure/article.html#Collections
? 作者:R. Mark Volkmann
? 译者:RoySong
集合(Collections)??? Clojure提供了list, vector, set 和map集合类型。Clojure同样可以采用任何Java集合类,但这并不经常出现,因为Clojure本身的集合更适合于函数式编程一些。
?
??? Clojure的集合类型拥有完全不同于Java集合的特性。它们都是不可变的、多相的、持久化的。不可变意味着集合的内容不能够被改变。多相意味着集合可以包含任意类型的对象。持久化意味着在集合的新版本创建时,旧版本依然被保存起来。Clojure采用了一种高效的方式来实现这个,即新旧版本共享内存。举个例子,一个新版本的map包含了上千个键值对,但比起旧版本来只改变了其中一个键值对,在共享内存的情况下,只需要消耗一点点额外的内存就可以创建新的版本。
?
??? 存在很多核心函数来操作各种类型的集合...实在是太多以至于无法在这儿详细描述。它们的一个小子集将在接下来被描述。记住一点,Clojure的集合是不可变的,没有函数能够改变它们。与之替代的,有很多函数采用 persistent data structures的特性高效地从一个已存在集合创建出一个新的来。同样的,一些函数操作某个集合(比如:vector)而返回另一个类型的集合(比如:LazySeq),它们也拥有不同的特性。
?
警告:这一节包含学习Clojure集合的重要信息,然而,它有些单调无味,呈现出的是一个接一个的函数操作各种类型的集合。如果睡意弥漫,就跳到下一节的开头,以后再来看这一段算了。
?
??? count函数计算任意类型的集合包含的元素数量,例子如下:
?
???
conj函数,是conjoin的简写,能够为某个集合添加一个或者多个元素。新元素被添加到什么位置取决于集合的类型。这将在我们接触到特定集合类型时来解释。?
???
reverse函数能够将集合中的元素倒序排列:?
??? 也有很多函数是从集合中检索出特定的多个值,举例如下;
?
??? 有很多断言函数来测试集合中每个值是否满足某种条件,并返回布尔结果。它们都是“短路运算”,只会对返回结果所必需的元素求值。举例如下:
?
???
some函数可以被用来确定某个集合是否包含指定的元素,它接受某个断言函数和某个集合作为参数。
或许定义某个断言函数来找出集合中是否存在某个指定元素确实是乏味的,实际上并不鼓励这种做法。
在list中查找某个确定元素是一种线性操作。采用set来代替list明显更高效且简单。
不管怎样,下面的例子展示了如何查找:?
???
into 函数创建一个包含两个list所有元素的新list。例子如下:?
? ?
peek和pop函数能够将list当作栈来操作,它们都操作list的开始或者头元素。?
Vectors??? Vector也是有序集合。当从结尾处添加或者删除元素时(常数时间),vecor的表现很完美。这意味着采用
conj函数来添加元素比cons要更加高效。在vector中通过索引来检索(采用nth)或者改变(采用assoc)元素的表现都很高效(常数时间)。函数定义时采用vector来作为参数列表。?
??? 这是一些创建vector的方法:
?
??? 除非特别需要从头部或者开始去添加或者移除元素才采用list之外,一般场合下,vector的表现都要优于list。这主要是由于vector的语法
[...]要比list的语法'(...)更加动人一点。它不会在函数、宏或者特殊form的调用中造成困扰。?
???
get函数在vector中根据索引检索出对应的元素。在之后的展示中,它同样在map中根据key检索出对应的值。
索引从0开始。get函数和nth函数很类似。它们俩都可以接受一个可选参数作为索引越界时的返回值。
如果没有这个参数,而索引越界了,get函数会返回nil,而nth函数会抛出一个异常。例子如下:?
???
subvec函数会返回一个已存在vector的子集,其中的元素排序仍保留。它接受一个vector参数,
一个起始索引参数,一个可选的结束索引参数。如果结束索引参数被省略,
结果子集的末尾就是原来vector的末尾。新的结果vector同原来的vector共享结构。?
??? 之前所有作用于list的代码示例都同样可以作用于vector上,
peek和pop 函数同样可以作用在vector上,但操作的是集合的尾部,而不像作用在list上操作的是集合的头部。conj 函数会创建一个尾部添加了新元素的vector。cons 函数则会创建一个头部添加了新元素的vector。?
sets??? set是元素唯一性的集合。当集合中元素不允许重复且不需要按次序添加时,比起list和vector来,set是较好的选择。Clojure支持两种类型的set,有序的和无序的。添加到有序set的元素不能彼此比较,否则会抛出一个
ClassCastException。下面是创建set的数种方式:?
???
contains?函数可以作用于set和map上面。当它作用于set上面时,用于确定set是否包含某特定元素。这比采用some函数在list或者vector检索元素要简单多了,见如下示例:???? 上面展示的作用于list的
conj和into 函数同样可以作用于set。不过只有有序set才能定义添加新元素的位置。?
???
disj 函数创建一个新的set,内容是移除了当前set的一个或者多个元素。例子如下:?
??? 同样在
clojure.set 命名空间中包含了如下函数:difference,index,intersection,join,map-invert,project,rename,rename-keys,select和union。其中的某些函数是作用于map而不是set上的。?
Maps??? map存储key以及对应的值,其中key和值都能够是任何类型。通常map的key采用关键字,值则基于结对的key采用有序便于获取的方式存储。
?
??? 下面是创建map的数种方式,以棒冰的颜色做为key,口味做为值,都采用关键字来存储它们之间的关系。采用逗号分隔可以帮助理解,这些逗号是可选的,在编译和运行时会被当作空白来处理。
???? 上面代码的输出结果是:
?
???
accessor 函数创建一个在实例中根据key获取对应值的访问函数,以避免执行hashmap寻址。例子如下:章节中讨论过,会强制元素的求值到一个或者多个延迟序列中。
for宏,在同样的章节中讨论过,并不强制求值,而是返回另一个延迟序列。?
??? 在执行时仅仅简单地需要一个副作用的情况下,采用
doseq或者dorun函数是合适的。执行的结果不会被保存,这样就能节省相当的内存。这两个函数的返回值都是nil。而当返回结果需要保存时,采用
doall函数就是合适的了,它保持了序列的头让结果可以被缓存起来,并返回对序列的求值。
?
??? 下面列表阐明了对延迟序列中的元素强制求值的选项:
?
保留执行结果 抛弃执行结果仅产生副作用 在单一序列上操作doalldorun采用列表包含语法操作任意数量的序列N/Adoseq?
??? 比起dorun来,优先采用
doseq函数,因为代码的可读性要高些。它同样更快些,因为在dorun里面对map的调用会创建一个新的序列。举个例子,下面两行代码会有一样的输出:
???? 然后我们对上面的程序做个改变,不在绑定中保持延迟序列的头。注意如何将已定义的序列做为函数的返回结果而不是
绑定值。这样就并未缓存集合中元素求值的结果,这样会降低内存占用,但是比起之前的程序来说,对集合中元素的请求
会更加低效。
(defn consumer [seq] ; Since seq is a local binding, the evaluated items in it ; are cached while in this function and then garbage collected. (println (first seq)) ; evaluates (f 0) (println (nth seq 2))) ; evaluates (f 1) and (f 2)(consumer (map f (iterate inc 0)))?