1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
|
+++
title = 'Coffee Break Clojure, Collections'
subtitle = '只用圆括号怎么够?'
slug = 'clj3'
aliases = '/article/tutorial-clj-3'
tags = ["Lisp","Clojure","编程"]
categories = '茶歇蔻芥'
date = 2026-09-26 18:20:05
draft = false
toc = true
fedi = 'https://akk.eltr.ac/objects/fab1fbe1-dc87-4a2e-8c5a-fc9ab9942c55'
endnotes = [
'推荐阅读:《[Collections and Sequences in Clojure](https://www.brainonfire.net/files/seqs-and-colls/main.html)》'
]
+++
[上一篇](/article/tutorial-clj-2)我们讨论了不同类型的形式,了解了函数、Lambda、宏和特殊形式的皮毛,也学会了用一些基本的运算符、`let` 和 `if` 等等。今天的文章要讨论集合数据类型。不过在此之前,我们还是先看看普通的数据类型有哪些。<!--more-->
今天的文章有点长,所以读的时候,呃…… 准备一杯大号的咖啡?
## 你已经知道和你可能不知道的
和大部分编程语言一样,Clojure 支持数字、浮点数、字符串和布尔值。
由于 Clojure 是运行在 JVM 上的,所以这些数据类型都对应一个 Java 类。数字是 `Long` 不是 `Int`,前者能表示的最大数是 2 的 63 次方减 1,也就是有 64 位,第一位是符号位,后者是 32 位。
和 Java 一样,Clojure 没有无符号数(unsigned int)。
Clojure 还能在这些类型上调用 Java 的方法,比如 `String.getBytes()`。
```clojure
(seq (.getBytes "Hello"))
;; => (72 101 108 108 111)
(type (first (seq (.getBytes "Hello"))))
;; => java.lang.Byte
```
Clojure 也没有用更小的 `Float`,而是用了双精度浮点数 `Double`。
```clojure
(type 1.1)
;; => java.lang.Double
```
Clojure 是门动态类型语言,一般不需要考虑太多类型问题,不过知道这些类型底层是什么样的也挺好。
值得注意的是 Clojure 还有个很常用的自定义类型,叫作关键词(keyword)。
```clojure
(type :keyword)
;; => clojure.lang.Keyword
```
关键词类似字符串,不过是以 `:` 开头,中间不能有空格。其实他更像其他语言里的枚举类型(enums),但无需提前定义。关键词不能像字符串一样做裁剪、找子串、当作字符数组遍历之类的操作,就是一个整体。
必要的时候,可以用 `name` 函数将它转换为字符串。
```clojure
(name :keyword)
;; => "keyword"
```
关键词可以用在大部分需要枚举的场合,也用来表示映射里的键(key)。
其他的非集合类型还有我们上一篇讨论过的符号(Symbol)。
## 集合数据类型与序列类型
集合数据类型统称 *Collections*,一般缩写做 `coll`,你会在 Clojure 的文档和各种函数里见到这个缩写。要判断一个数据是否是集合,可以用谓词函数 `coll?`:
```clojure
(coll? 1) ;; => false
(coll? "string") ;; => false
(coll? '(1 2 3)) ;; => true
(coll? [1 2 3]) ;; => true
(coll? {:key "value"}) ;; => true
(coll? #{1 2 3}) ;; => true
;; 当然,代码即数据
(coll? '(println "Hello World")) ;; => true
```
你之后还会见到很多 `?` 结尾的谓词函数。谓词函数(predicate)是指返回布尔值的函数,在编写自己的函数时,最好也按照这种规则命名。这就像…… Java 的 `IsXXX()` 方法一样。
上面见到的四种有不同括号标记的集合数据类型就是今天的主角。在正式介绍他们之前,我还想讲讲序列(sequence)。序列和集合分别对应两个 Java 接口:`ISeq` 和 `IPersistentCollection`。下面是 `ISeq` 的定义:
```java
public interface ISeq extends IPersistentCollection {
Object first();
ISeq next();
ISeq more();
ISeq cons(Object o);
}
```
`ISeq` 继承了 `IPersistentCollection`,可以理解为:序列是集合的子集。可以看到这个接口里的访问方法只有 `first()`、`next()` 和 `more()`,也就是说序列只支持**线性访问**,不能**随机访问**,也就是没有索引(index)和键(key)。
可以用 `seq?` 判断一个集合数据是不是序列。
```clojure
(seq? '(1 2 3)) ;; => true
(seq? [1 2 3]) ;; => false
(seq? {:key "value"}) ;; => false
(seq? #{1 2 3}) ;; => false
```
只有列表是序列,不过其他数据类型都能转换为序列。
```clojure
;; 没有 ? 的 seq 用于把集合转换为序列
(seq [1 2 3]) ;; => (1 2 3)
(seq {:key "value"}) ;; => ([:key "value"])
(seq #{1 2 3}) ;; => (1 3 2)
;; 字符串可以变成字符序列
(seq "hello") ;; => (\h \e \l \l \o)
```
只有序列才能够遍历,Clojure 中许多集合相关的操作都会**自动**把集合数据转换为序列。注意看,映射被转换为序列后,每个元素都是一个包含键和值的向量,所以之后我们遍历映射的时候,其实是在遍历向量序列。
序列并不是具体的数据类型,集合(collection)也不是,它们是对列表、向量、映射、集(set)以及惰性序列(lazy seq)的抽象。只不过许多操作都基于抽象的类型进行,这样很多函数就能通用。
## 圆括号和列表
我们在之前就见过列表(list),最普通的集合数据类型。
```clojure
(list "string" 1 :item true)
;; or
'("string" 1 :item true)
```
和许多动态类型语言一样,列表可以包含不同类型的数据。
注意,上面这个例子中两种创建列表的方式有区别,我在[第零篇](/article/tutorial-clj-0)留了个习题,现在公布答案:
```clojure
(list 1 (+ 1 1) 3)
;; => (1 2 3)
'(1 (+ 1 1) 3)
;; 猜猜这段代码会得到什么结果?
;; (1 (+ 1 1) 3)
```
`quote` 会「冻结」列表内的一切,嵌套的形式也不会被求值。我们在上一篇讲到,调用函数前需要把所有参数都求值。`list` 就是个函数,`(+ 1 1)` 求值得到 `2` 才会被传入 `list`。
在更「原教旨」的 Lisp 中,`list` 是 `cons` 的一种简便写法。`cons` 的意思是 *construct*,被称作「列表构造函数」。一个 `cons` 只能造一个有序对,`(cons 1 2)` 会返回 `(1 . 2)`,中间的点标记表示这是个 *cons* 对(叫 *cons pair* 或者 *cons cell*)。
`cons` 构造列表的方式有点像个链表,`cons` 左边存放的是列表的一个元素,右边存放的是下一个 *cons* 对,如果列表结束了,右边存的就是 `NIL`。
```lisp
(cons 42 (cons 69 (cons 613 nil)))
;; 等价于
(list 42 69 613)
```

{.dark:invert}
虽然 Clojure 的 `cons` 也可以用相同的方式构造列表,但 *cons* 对却不存在。`cons` 的第二个函数只能是序列或者 `nil`。
```clojure
(cons 1 2)
;; Execution error (IllegalArgumentException) at user/eval30 (REPL:1).
;; Don't know how to create ISeq from: java.lang.Long
```
可以说 Clojure 弱化了 `cons` 作为列表构造函数的身份,`cons` 只被用来「把数据放进列表的前面」。
```clojure
(cons 1 '(2 3))
;; => (1 2 3)
```
Clojure 的列表叫作持久化列表(PersistentList),Clojure 的所有集合数据类型都是持久化(Persistent)且不可变(Immutable)的。
```clojure
(type '(1))
;; => clojure.lang.PersistentList
```
不可变很好理解,对数据做操作只会产生新的数据,而不会修改原来的数据,原来的数据还保存在内存的同一个地方。那什么是[持久化](https://zh.wikipedia.org/wiki/%E5%8F%AF%E6%8C%81%E4%B9%85%E5%8C%96%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84)呢?意思是数据被修改后,历史的版本还保留着并且还**能被访问**。
不妨看看 Clojure 是怎么实现 `PersistentList` 的:
```java
public class PersistentList extends ASeq implements IPersistentList, IReduce, List, Counted {
private final Object _first;
private final IPersistentList _rest;
private final int _count;
```
注意看 `_first` 和 `_rest` 这两个私有变量。`_first` 的类型是 `Object`,在 Java 中,这是所有类的基类,作用类似于 Go 的 `any`,任何类型传入都能通过类型检查。`_rest` 的类型是 `IPersistentList`,这是持久化列表的接口,`PersistentList` 就实现了这个接口。
不难看出这和 *cons* 非常类似:`_first` 存的是列表节点的值,`_rest` 存的是下一个值的「指针」。在原教旨 Lisp 里,`_first` 和 `_rest` 分别对应 `car` 和 `cdr`(读作 *coulder*)[^1]。
我们来看看 `PersistentList` 是怎么实现 `add()` 的:
```java
public void add(int index, Object element){
throw new UnsupportedOperationException();
}
```
嗯?直接抛出异常了?
令人费解,而且违反了里氏替换原则。仔细想想,Clojure 的确没有给列表直接添加元素的方法,因为列表不可变,我们用的是 `cons` 和 `conj`(后者与 `cons` 相反,是在序列的尾部添加元素)。那 `cons` 是怎么实现的?
```java
public Cons(Object first, ISeq _more){
this._first = first;
this._more = _more;
}
```
这下就清晰了,求值 `(cons 1 '(2 3))` 得到 `(1 2 3)`,原来的 `(2 3)` 留在原地,我们只是创建了一个 `Cons` 存放 `1`,指向 `(2 3)` 这个序列,而 `Cons` 本身也是个序列,可以再用它创建新的 `Cons`,新的 `Cons` 也会就会指向它。
这种构造持久化列表的方式叫作结构共享(Structural Sharing),既做到了不可变,又没有在内存中管理大量的数据副本。本质上,一切都是指针。
由于不可变,所以列表的大小可以作为不变的属性保存起来,回去看 `PersistentList` 的 Java 代码,`_first` 和 `_rest` 下面就是 `_count`。Clojure 的所有集合数据类型都能用 `count` 函数获取其长度,并且这个算法的时间复杂度是 *O(1)*,因为只需要 `return _count`。此外,有很多操作都因为结构共享变得非常高效,Clojure 的性能在 Lisp 中算很好的那一类。
至于不可变还有什么好处,我们之后谈函数式编程再提。
## 方括号和向量
理解一门 Lisp 中的列表如何构造,可以说就理解了它的核心。我们知道列表是持久化且不可变的,由 `Cons` 和指向其他 `Cons` 的「指针」构成。
向量基本上和列表一致,但向量中所有元素都有索引(index),这点和 JavaScript 的数组很像,写起来也和 JavaScript 数组很像,也可以存放不同类型的数据。
```clojure
[1 "I'm a vector" :vector true]
```
向量和列表最大的不同点在于,可以用索引访问向量的元素,但列表没有索引。
```clojure
(get [1 2 3 4] 2) ;; => 3
(get '(1 2 3 4) 2) ;; => nil
```
向量和列表的另一个区别,尽管他们都是连续的(sequential),但只有列表是序列(sequence)。用 `seq?` 检查数据是不是序列的时候,向量会返回 `false`。
```clojure
(seq? '(1 2)) ;; => true
(seq? [1 2]) ;;=> false
```
如果希望向量也返回 `true`,就要检查一个集合有没有实现 `Sequential` 接口。
```clojure
(sequential? '(1 2)) ;; => true
(sequential? [1 2]) ;; => true
```
有意思的是,把 `seq?` 的问号去掉,你也能让向量返回一个真值。
```clojure
(if (seq [1 2])
"I can be a seq"
"I can not be a seq")
;; => "I can be a seq"
```
`seq` 的作用是把可以转换为序列的数据结构转换为序列,集合只要不是空的,都会返回一个正常的序列,会被条件判断当作真值。因此 `seq` 经常被用来检查一个集合是否**不为空**;相反,`emtpy?` 用来检查一个集合是否**为空**。
要注意的是,如果对向量做了序列操作,比如用 `rest` 函数获取向量的第一个元素之后的所有元素,就会得到一个序列,向量的索引就不可用了:
```clojure
(rest [1 2 3])
;; => (2 3)
(get (rest [1 2 3]) 1)
;; => nil
```
可以用 `vec` 把序列变成向量
```clojure
(get (vec (rest [1 2 3])) 1)
;; => 3
```
可以用 `vector?` 检查数据是不是向量。
```clojure
(vector? [1 2 3])
;; => true
(vector? (seq [1 2 3]))
;; => false
```
最后,向量其实还继承了 `AFn` 类,可以当成函数使用。
```clojure
([1 2 3] 0) ;; => 1
;; 等价于:
(get [1 2 3] 0) ;; => 1
```
这样就省掉了写 `get` 的工夫。
## 花括号和映射
Clojure 的映射(map)写起来和 JSON 很像。
```clojure
{:key1 "val1"
:key2 {:subkey1 2
:subkey2 [1 2 3]}
:key3 nil}
```
映射和向量也很像,映射不过是可以自定义索引的向量。向量只能用从 `0` 开始自增的整数索引,但映射可以自定义键。键一般是关键词类型,但也可以是别的类型,比如这是 HTTP 响应头的映射表示:
```clojure
{"Content-Type" "text/html; charset=utf-8"
"Cache-Control" "no-cache"
"Date" "Sat, 26 Sep 2026 07:06:35 GMT"}
```
一般而言,推荐使用关键词表示键,一方面是看起来更清晰,另一方面是,关键词和向量一样,可以当作函数使用,从映射里取出值:
```clojure
(:key1 {:key1 "val1"})
;; => "val1"
;; 等价于
(get {:key1 "val1"} :key1)
;; => "val1"
```
其他类型却不行:
```clojure
(let [data {"Content-Type" "text/html; charset=utf-8"
"Cache-Control" "no-cache"
"Date" "Sat, 26 Sep 2026 07:06:35 GMT"}]
("Content-Type" data))
;; Execution error (ClassCastException) at user/eval194 (REPL:4).
;; class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
;; 只能写作:
(get data "Content-Type")
```
可以引入 `clojure.walk` 标准库,用这个库里的 `keywordize-keys` 方法,把不是关键词类型的键全部转换为关键词。
```clojure
(require '[clojure.walk :as walk])
(walk/keywordize-keys {"Content-Type" "text/html; charset=utf-8"
"Cache-Control" "no-cache"
"Date" "Sat, 26 Sep 2026 07:06:35 GMT"})
;;=> {:Content-Type "text/html; charset=utf-8",
;; :Cache-Control "no-cache",
;; :Date "Sat, 26 Sep 2026 07:06:35 GMT"}
```
其实把关键词和映射反过来也能达到一样的效果,因为映射和向量一样,也继承了 `AFn` 类,可以当作函数用。
```clojure
({:key1 1 :key2 2} :key2)
;; => 2
(:key2 {:key1 1 :key2 2})
;; => 2
```
由于映射可以嵌套,你可能会遇到需要获取一连串键值对的情况,这时候就用 `get-in`。比方说,我们可能要处理结构非常*反人类*的 [microformats](https://microformats.org/)。
```clojure
{:webmention {:source "https://alice.blog"
:target "https://bob.net"
:microformats [{:type ["h-card"]
:properties {:photo [{:alt "Photo of Alice"
:value "https://alice.blog/photo.avif"}]
:name "alice"
:url "https://alice.blog"}}]}}
```
```clojure
(get-in data [:webmention :microformats 0 :properties :photo 0 :value])
;; => "https://alice.blog/photo.avif"
```
和其他集合类型一样,映射不需要写逗号分隔元素,但写了也不会报错。如果你觉得这样更清晰,可以在每个键值对后面写上逗号。==不过无论有没有逗号,奇数个元素都是键,偶数个元素都是值==。如果映射里的元素没有成对,就会报错。
```clojure
{:key nil :key2}
;; Syntax error reading source at (REPL:51:17).
;; Map literal must contain an even number of forms
```
键也不能重复:
```clojure
{:key nil :key nil}
;; Syntax error reading source at (REPL:52:20).
;; Duplicate key: :key
```
试图用 `cons` 或 `conj` 把一个键值对放进映射里,就算键重复也不会报错,因为 `cons` 返回的是个序列,并非映射。
```clojure
(cons [:key1 nil] {:key1 nil})
;; => ([:key1 nil] [:key1 nil])
```
要往映射里添加键值对,或者覆盖某个键的值,要用 `assoc`(*associate*)。
```clojure
(assoc {:key1 nil} :key2 nil)
;; => {:key1 nil, :key2 nil}
(assoc {:key1 nil} :key1 1)
;; => {:key1 1}
```
如果要移除某个映射,就用 `dissoc`(*dissociate*)。
```clojure
(dissoc {:key1 nil} :key1)
;; => {}
```
## 井号和集
在 `{}` 前面加上 `#`,就会得到集(set)。*Set* 其实应该叫集合,但我担心和 *Collections* 混淆,所以称作「集」。顾名思义,集就是不能有重复数据的集合。
```clojure
#{1 1 2 3}
;; Syntax error reading source at (REPL:61:11).
;; Duplicate key: 1
```
注意看,这里的报错是 `Duplicate key`(重复的键)。实际上,集就是把键和值合二为一的映射,不妨试试用关键词作为函数,取出集合里的关键词。
```clojure
(:key1 #{:key1 :key2 :key3})
;; => :key1
```
向量和列表都不能这么干,因为向量的键是整数,而列表没有键。
```clojure
(:key1 [:key1 :key2])
;; => nil
(:key1 '(:key1 :key2))
;; => nil
```
向量和映射都可以作为函数,集合也可以。
```clojure
(#{1 2 3} 2)
;; => 2
(#{1 2 3} 100)
;; => nil
```
如果参数存在于集合中,就会返回集合中的这个元素;如果不存在,就会返回 `nil`。由于 `nil` 是假值,所以可以用这种方式判断某个元素是否存在于集合中。
```clojure
(let [allowed-domains #{"kagi.com" "duckduckgo.com"}]
(if (allowed-domains "google.com")
"Domain allowed"
"Domain not allowed"))
;; => "Domain not allowed"
```
## 检查集合
我们已经知道,可以用 `seq` 检查集合是否非空,用 `empty?` 检查集合是否为空;可以用 `seq?`、`vector?`、`list?`、`sequential?`、`map?` 和 `set?` 检查数据是否为序列、向量、列表、连续的、映射或集合。
除了对集合数据本身做检查,我们可能还想要对集合里面的元素做检查。
用 `every?` 检查集合里的**每个元素**是否都满足条件。
```clojure
(every? even? [2 4 6])
;; => true
(every? odd? [1 2 4])
;; => false
```
`every?` 接收两个参数,第一个参数 `pred` 是直接接收一个变量的谓词函数,第二个参数 `coll` 是要检查的集合。这个函数对每个元素求值一次 `(pred element)`,只要有一个是 `false`,就返回 `false`,否则返回 `true`。
`even?` 和 `odd?` 分别检查数字是否为奇数和是否为偶数。
第二个参数可以是匿名函数 `fn`:
```clojure
(every? (fn [x] (= x 1)) [1 1 1])
;; => true
```
匿名函数可以简写为 `#()`,用 `%` 表示参数:
```clojure
(every? #(= % 1) [1 1 1])
;; => true
```
如果不要求所有元素,只要求某一些元素符合要求,就用 `some`。
```clojure
(some #(= % 1) [1 2 3])
;; => true
(some #(= % 1) [2 3 4])
;; => false
```
为什么 `some` 没有 `?` 呢?那是因为 `some?` 是另一个函数,用来判断某个值是否**不是** `nil`。
```clojure
(some? 0)
;; => true
(some? [1 2 3])
;; => true
(some? nil)
;; => false
```
用来判断某个值**是** `nil` 的函数是 `nil?`,其实 `some?` 就是 `(not (nil? x))`,[源代码](https://github.com/clojure/clojure/blob/clojure-1.12.4/src/clj/clojure/core.clj#L533)就是这么写的。
有点绕,对吧?还有更绕的。
Clojure 新手常见的问题是搞不清楚 `contains?` 的作用。一般人会下意识地以为这个函数用来检查集合是否*包含*某个元素,但很快发现并非如此。
```clojure
(contains? [5 7 8] 5)
;; => false
(contains? [5 7 8] 0)
;; => true
```
`contains?` 用于检查集合中是否包含某个键(key)。我们知道向量就是键为自增整数的映射,`[5 7 8]` 中各个元素的键或者说索引分别是 `0 1 2`,所以用 `contains?` 查找 `5` 会返回 `false`,但查找 `0` 会返回 `true`。
`contains?` 可以用来测试映射中是否包含某个键,集是否包含某个元素:
```clojure
(contains? {:key1 "value"} :key1)
;; => true
(contains? #{1 4 2} 4)
;; => true
```
如果要检查列表和向量中是否包含某个值(而非键),应该用什么呢?其实你已经会了。仔细想想,用上面提到过的哪些函数可以做到这点?
<details>
<summary>答案揭晓</summary>
如果你的答案是:
```clojure
(some #(= % :want) [:some :element :here])
;; => false
(some #(= % :want) [:i :want :to :learn :clojure])
;; => true
```
恭喜你对了一半!更 *Idiomatic* 的写法是:
```clojure
(some #{:want} [:some :element :here])
;; => nil
(some #{:want} [:i :want :to :learn :clojure])
;; => :want
```
还记得吗?集也可以当作函数用。
</details>
## 操作集合
如果你第一时间想到的是用 `for` 循环遍历和操作集合,~~那你可得好好反思一下~~。别担心,我刚开始也一直试图弄懂怎么在 Clojure 里写循环结构(也就是 `for`、`while`、`do ... while`),后来发现,我根本不需要。至少在我目前写 Clojure 的一百多个小时里,我一次也没有用过 `for` 和 `while`。
### `filter` 和 `remove`
假设我们有这样一个列表。
```clojure
(def numbers
'(1 43 231 64 23 99))
```
我希望删除里面的偶数,我该怎么办?这样吗?
```clojure
;; (take n coll) 返回由 coll 前 n 个元素组成的序列
;; (drop n coll) 返回丢弃 coll 前 n 个元素后的序列
;; (concat coll coll) 把两个集合拼接起来
;; (inc n) 相当于 (+ 1 n)
(for [n numbers]
(if (even? n)
(let [i (.indexOf numbers n)]
(concat (take i numbers) (drop (inc i) numbers)))
numbers))
;; => ((1 43 231 64 23 99)
;; (1 43 231 64 23 99)
;; (1 43 231 64 23 99)
;; (1 43 231 23 99)
;; (1 43 231 64 23 99)
;; (1 43 231 64 23 99))
```
*欸?不对,Clojure 数据是不可变的,我只是返回了很多个新的列表,其中有一些是去除了一个偶数的。照理来说,我应该声明一个空列表,然后把不是偶数的元素放进去…… 欸?Clojure 的赋值语句是什么?嗯?Clojure 是函数式语言,那是什么意思?啊?没有赋值语句,绑定了符号就不能改了?*
我的朋友,你的问题,只需要一行代码解决:
```clojure
(remove even? numbers)
;; => (1 43 231 23 99)
```
或者这样:
```clojure
(filter odd? numbers)
;; => (1 43 231 23 99)
```
和刚才的 `every?` 和 `some` 一样,`remove` 和 `filter` 接收一个谓词函数和一个集合,分别用来移除符合条件的元素,或保留符合条件的元素。
### `reduce`
现在我又有新的需求了,我要把这些数字累加起来,应该怎么做?
```clojure
(reduce + numbers)
;; => 461
```
`reduce` 接收函数 `f` 和集合 `coll`,其中 `f` 应该要能接收两个参数。它先对前两个元素求值 `(f x y)`,然后对下一个元素求值 `(f (f x y) z)`,以此类推。这里我们用的是 `+`,所以就实现了累加。
还可以传入初始值,这样 `reduce` 就不会从 `(f x y)` 开始求值了。我们能用它来拼接集合。
```clojure
(reduce conj [1 2] [3 4])
;; => [1 2 3 4]
;; 直接用 conj 的话,第二个参数会被当作一整个元素放进去
(conj [1 2] [3 4])
;; => [1 2 [3 4]]
```
`(reduce conj a b)` 的简便写法是 `into`:
```clojure
(into [1 2] [3 4])
;; => [1 2 3 4]
;; concat 也能拼接集合,但返回的是序列
(concat [1 2] [3 4])
;; => (1 2 3 4)
;; 操作向量时,最好用 into
```
练习一下,如果要把下面这个向量打平(flatten)然后去重,应该怎么做?
```clojure
(def flatten-me
[[1 2] [53 13 5] [2] [12 53 55]])
```
<details>
<summary>答案揭晓</summary>
```clojure
(reduce into [] flatten-me)
;; => [1 2 53 13 5 2 12 53 55]
```
去重只需要把向量变成集合,然后再变回来。
```clojure
(vec (set (reduce into [] flatten-me)))
;; => [1 55 13 12 2 5 53]
```
</details>
### `map`
如果我想要把列表里的数字全部翻倍,应该怎么做?
`map` 函数接收一个函数 `f` 和一个集合 `coll`,把 `f` 作用在每个元素上之后,返回新的集合。
```clojure
(map #(* % 2) numbers)
;; => (2 86 462 128 46 198)
```
要注意的是,`map` 返回的是个序列(准确来说是惰性序列,后面再讲)。如果想要返回向量,就换成 `mapv`。
```clojure
(map #(* % 2) (vec numbers))
;; => (2 86 462 128 46 198)
(mapv #(* % 2) (vec numbers))
;; => [2 86 462 128 46 198]
```
## 箭头宏
单用上面这些函数中的任何一个是无法实现复杂需求的。假设我想要把列表中的偶数删掉,然后对剩下的奇数求平方,最后过滤掉超过 `1000` 的数,再累加起来。你可能就能感受到什么是 *Lost In Stupid Parentheses* 了:
```clojure
(reduce + (filter #(<= % 1000) (map #(* % %) (remove even? numbers))))
;; => 530
```
可以发现这些函数全都是类似的形式:`(func f coll)`。函数放在中间,集合放在最后面。有一个宏,可以自动把他的参数嵌套在一起,把上一个参数放进下一个参数的末尾。
```clojure
(->> numbers
(remove even?)
(map #(* % %))
(filter #(<= % 1000))
(reduce +))
;; => 530
```
`->>` 叫作 *thread-last*,似乎也有翻译成「线程宏」的,但 *thread* 只是把层层嵌套的函数调用比喻成一条线而已,或许可以叫「线宏」,但我更喜欢叫它「箭头宏」。
*thread-last* 是说,把上一个函数形式放进下一个函数形式的最后一个参数的位置。自然还有 *thread-first*,写作 `->`。如果要传递和操作的数据都在第一个参数的位置,或者这条线上的函数都只接收一个参数,就可以用 `->`。
学会使用箭头宏,在 Clojure 里你经常需要把数据在函数之间传来传去,它能帮你少写很多括号,而且代码会更简单易读。
---
## 最后
我已经写了五千多字,还没写到惰性。我也犯懒了,读者如果着急,可以先阅读我今年年初写的《[计算无穷](/article/compute-infinity/)》来了解惰性是什么。
另外,最近有不少读者表达了对这个系列的喜爱,很高兴看到有人对 Clojure 感兴趣!希望我突然写这么长一篇文章没有把你们吓到。没掌握也没关系,可以随时会来查阅。之后需要用到集合的地方很多,有很多机会练习。
回见!
[^1]: 可参考:《[Just A Common Lisper](/article/common-lisp-start/)》
|