JohnShen's Blog.

Clojure in Action: 概述、数据结构

字数统计: 7.1k阅读时长: 31 min
2019/07/25 Share

Clojure 概述

关键词:JVMLisp动态类型函数式语言不可变数据结构Code as data

1. Clojure 是 Lisp 的一个变种

Clojure 对所有函数甚至类似运算符的一切都使用前缀表示法。

2. Clojure 以JVM为宿主

Clojure 代码直接编译为字节码供JVM运行;

Clojure 默认加载 java.lang包下的所有类;

Clojure 直接使用 Java 类型和标准程序库,如 Clojure 的集合实现接口与 Java 集合接口相同,重用 Java 类型和接口可以使 Java 代码无缝使用 Clojure 类型;

使用点运算符作为与Java 互操作的基础:(. Math abs -3)(. "foo" toUpperCase),静态成员可以重写为(Math/abs -3),实例方法调用也可以使用(.toUpperCase "foo"),创建类的实例可以使用(new Integer "42")(Integer. "42")

Clojure 的不可变数据结构使共享可变状态的问题变得毫无意义。即使需要变更状态,Clojure 也提供了var(变量)、atom(原子)、ref(引用)、agent(代理)的并发数据结构。

3. Clojure 是一种函数式编程语言

函数是第一等公民,函数可以作为参数传递给其他参数,也可以作为输出值返回。FP设计的函数式纯粹的,具备引用透明性,只要函数输入相同就始终返回相同输出;

FP一般默认不可变数据结构,将不可变结构作为语言的默认状态保证了函数不会修改传递给他们的参数。Clojure的不可变数据结构避免了高代价复制。当对一个不可变数据结构进行更改,结果则为一个全新的结构。Clojrue隐式使用结构化共享和其他技术,确保执行复制的次数最少、不可变数据结构的操作便捷且节约内存。比如在一颗树上添加新值,会在通往根节点的路径上创建一组新的节点和引用;

Clojure鼓励使用纯函数式编程:不可变数据结构、高阶函数和代替强制循环的递归,甚至可以选择集合的惰性求值和及早求值。当然,为了适应不同场景,Clojure也提供了对共享状态变更的方法。

Clojure 基础

前期准备

1. Clojure REPL(读取 - 求值 - 打印循环)

1
2
3
4
5
6
7
8
9
10
11
(+ 1 2)                                                   
;=> 3
(def my-addition (fn [operand1 operand2] (+ operand1 operand2)))
;=> #'user/my-addition
(my-addition 1 2)
;=> 3
(my-addition 100 30)
;=> 130
(+ 1 2) "Two forms on one line!"
;=> 3
;=> "Two forms on one line!"

第二个表达式定义了一个命名空间限定的全局符号user/my-addition 。前缀#'表明这是一个 Clojure 变量,变量是一个可变容器,其中包含唯一值,本例中为加法函数。

函数中没有显示的 return 语句。从函数中返回的值总是函数中最后一个求值的表达式。

最后三行是按照形式运行,Clojure 持续读取,直到发现一个完整的形式,然后求值并打印,此后如果缓冲区里仍有字符,它读取另一个形式、求值并打印。

2. 特殊 REPL 变量

变量*1*2*3*e保存最后一个、倒数第二个、倒数第三个成功读取的形式和最后一个错误。每当新形式求值成功,该值会保存在*1,旧*1被移动到*2,旧*2被移动到*3

3. 文档查找

  • doc:返回具体的函数描述,该 宏需要你了解具体的实体名称。
  • find-doc:接受一个字符串(可以是正则),模糊查询复合条件的函数或宏文档。该函数在不确定名称时很实用。
  • apropos:工作方式与find-doc类似,只打印匹配搜索模式的函数名称。

4. 其他细节

  • 前缀表示法。没有任何运算符,数学运算符就是 Clojure 函数。

  • 空格。Clojure 不需要逗号来区分列表元素,当实用逗号时,Clojure 会把它们当成逗号忽略。当然,特定场景如哈希映射,使用逗号有助于程序员理解。

  • 注释。单行注释使用分号表示。多行注释可以使用comment宏,该宏会忽略传入的形式,返回 nil。此外,宏#_会告诉reader忽略下一个Clojure形式。

    1
    2
    [1 2 3 #_ 4 5]
    ;=> [1 2 3 5]
  • Clojure 大小写敏感。

Clojure 数据结构

1. nil / 真值 / 价值

Clojure 的 nil等价于 Java 的 null ,在 nil 上调用一个函数可能报空指针异常。

除了 false 和 nil 之外,其他值都被视为真值。

2. 字符 / 字符串

Clojure 字符是 Java 字符, 使用反斜杠宏表示字符:

1
2
(type \a)
;=> java.lang.Character

Clojure 字符串是 Java 字符串,使用双引号表示。(单引号则是另一个读取器宏,来表示 Symbol)

1
2
(type "hello")
;=> java.lang.String

Java String API 在 Clojure 中依然很有用。

3. 数值

Clojure 使用的整数是64位整数(Long),浮点数是64位浮点数(Double)。当需要更大的范围时,可以使用BigInteger,BigDecimal。此外,还有一个不常见的数值类型:比例(ratio),比例在两个整数相除时创建。

1
2
3
4
5
6
7
8
9
10
11
12
(type 2)
;=> java.lang.Long
(type 3.14)
;=> java.lang.Double
(type 1/3)
;=> clojure.lang.Ratio
(type true)
;=> java.lang.Boolean
(type 123N)
;=> clojure.lang.BigInt
(type 0.5M)
;=> java.math.BigDecimal

当不同数值类型在同一个算术运算中混合使用时,具有高传染性的数值类型将其类型传染给结果(long < bigint < ratio < bigdec < double)

1
2
3
4
5
6
7
8
(+ 1 1N)
;=> 2N
(+ 1 1N 1/2)
;=> 5/2
(+ 1 1N 1/2 0.5M)
;=> 3.0M
(+ 1 1N 1/2 0.5M 0.5)
;=> 3.5

溢出(overflow):在 Clojure 中可能产生溢出的算术运算只有整数加法、减法和乘法(整数相除时,如果超过范围则生成一个比例)。

溢出发生时 Clojure 会抛出 ArtithmeticException异常。如果希望结果提升为大整数,则应该使用:+'-'*'inc'dec'

1
2
3
4
(inc 9223372036854775807)
;ArithmeticException integer overflow clojure.lang.Numbers.throwIntOverflow (Numbers.java:1424)
(inc' 9223372036854775807)
;=> 9223372036854775808N

4. 符号 / 关键字

符号是 Clojure 中的标识符,代表值的名称。符号本身只包含可选命名空间的名称,但当一个表达式求值时,它们被所代表的值取代。

在一个程序中,符号通常被解析为不是符号的其他内容,但是可以通过一个前导的单引号引用符号,将其当成一个值而非标识符。

当为一个符号加上引号,就将这个引号当成数据而不是代码来处理,在实践中一般不会这么做,因为有另一种特殊类型:关键词,关键词从不引用其他值,求值的结果总是他们本身。关键词的典型用法是作为哈希映射中的键和枚举值。

5. 列表

主要函数:listlist?conjpeekpopcount

Clojure 列表是单链表。只能从列表前端添加或删除元素,这意味着多个不同列表可以共享相同尾部,使列表成为最简单的不可变数据结构。

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
(list 1 2 3 4 5)
;=> (1 2 3 4 5)
(list? *1)
;=> true

(conj (list 1 2 3 4 5) 6)
;=> (6 1 2 3 4 5)
(conj (list 1 2 3) 4 5 6)
;=> (6 5 4 1 2 3)
(conj (conj (conj (list 1 2 3) 4) 5) 6)
;=> (6 5 4 1 2 3)

; 可以将列表当成一个栈来对待
(peek (list 1 2 3))
;=> 1
(pop (list 1 2 3))
;=> (2 3)
(peek (list))
;=> nil
(pop (list))
;IllegalStateException Can't pop empty list clojure.lang.PersistentList$EmptyList.pop (PersistentList.java:183)

(count (list))
;=> 0
(count (list 1 2 3 4))
;=> 4

列表的特殊性:Clojure 会假定列表中出现的第一个符号表示函数(或者宏)名称。Clojure 视图以处理所有列表的相同方式来处理表(1,2,3) ,第一个元素被视为函数,而这里的整数 1 并不是函数。若希望将其作为数据而非代码,解决方式就是加上引号:'(1,2,3)

实践中一般不会在 Clojure 代码中使用列表作为数据,而是使用向量类型。

6. 向量

主要函数:vectorgetnthassoc, conj,peekpopcountsubvec函数本身

向量可以使用vector函数创建,也可以使用方括号表示法创建,可以快速随机访问向量中的元素。获取其中元素中的方法有getnth,差别在于没有找到相应值时,nth 会报出错误,get 返回 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
(vector 10 20 30 40 50)
;=> [10 20 30 40 50]
(def the-vector [10 20 30 40 50])
;=> #'user/the-vector

(get the-vector 2)
;=> 30
(nth the-vector 2)
;=> 30
(get the-vector 10)
;=> nil
(nth the-vector 10)
;IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:107)
nth Get Vector as fn
Vector is nil nil nil Throw exception
Index out of range Throw exception Nil Throw exception
Support ‘not found’ param yes Yes no

修改向量的方法中最常见的是 assoc

1
2
3
4
5
6
7

(assoc the-vector 2 25) ; 可以变更现有索引
;=> [10 20 25 40 50]
(assoc the-vector 5 60) ; 可以添加到尾部
;=> [10 20 30 40 50 60]
(assoc the-vector 6 70) ; 但是不能超过尾部
;IndexOutOfBoundsException clojure.lang.PersistentVector.assocN (PersistentVector.java:137)

conj函数同样适用于向量,但在向量中新元素将会被添加到最后,因为那是向量中最快速的插入位置。

1
2
(conj [1 2 3 4 5] 6)
;=> [1 2 3 4 5 6]

peekpop也适用于向量,方法会查看向量的尾部,而不是列表的表头。

1
2
3
4
5
6
7
8
(peek [1 2])
;=> 2
(pop [1 2])
;=> [1]
(peek [])
;=> nil
(pop [])
;IllegalStateException Can't pop empty vector clojure.lang.PersistentVector.pop (PersistentVector.java:381)

向量本身也是取单一参数的函数。(但向量函数不接受第二个参数)

1
2
(the-vector 3)
;=> 40

subvec会返回向量的一个子向量 (subvec avec start end?)。若未指定end,则默认为向量的末尾。

1
2
3
4
(subvec [1 2 3 4 5] 3)
;-> [4 5]
(subvec [1 2 3 4 5] 1 3)
;-> [2 3]

7. 映射

主要函数:hash-mapsorted-mapassocdissocselect-keysmergemerge-withassoc-inget-inupdate-inkeysvalscontains?get函数本身

一个映射就是一个键值对序列。映射可以使用hash-map函数构建。依据键获取对应的值时除了使用get函数外,映射本身也是一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(def the-map {:a 1 :b 2 :c 3})
;=> #'user/the-map
(hash-map :a 1 :b 2 :c 3)
;=> {:a 1, :c 3, :b 2}

(the-map :b)
;=> 2
(:b the-map)
;=> 2
(:z the-map 26) ; 若未找到关键字则返回一个默认值
;=> 26
(get the-map :z 26)
;=> 26
(the-map :z 26)
;=> 26

(sorted-map :c 3 :b 2 :a 1)
;=> {:a 1, :b 2, :c 3}

映射字面量和 hash-map 函数不完全等价,Clojure 实际上有哈希映射(hash-map)以及数组映射(array-map)。数组映射以有序方式保存键和值,以扫描的方式进行查找。如果使用 assoc 函数将太多键关联到一个数组映射,那将会得到一个哈希映射(而哈希映射变得太小不会反悔一个数组映射)。透明地替换数据结构的实现是 Clojure 提高性能的常用技巧。

此外,sorted-map不会按照存放的顺序返回,它会根据键来进行排序。

映射的修改方法有assocdissoc等。assoc 返回新增了一个键值对的映射表, dissoc 返回移除了某些键的映射表。 select-keys返回一个映射表,仅保留了参数传入的那些键。merge可以合并映射表。如果多个映射表包含了同一个键,那么最右边的获胜。merge-with与 merge 很类似,除了当两个或以上的映射表中有相同的键时,你能指定一个你自己的函数,来决定如何合并这个键对应的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(def updated-map (assoc the-map :d 4))
;=> #'user/updated-map
updated-map
;=> {:d 4, :a 1, :b 2, :c 3}
(dissoc updated-map :a)
;=> {:b 2, :c 3, :d 4}
(select-keys updated-map [:a :b])
;=> {:a 1, :b 2}
(merge updated-map {:m 666, :n 777})
;=> {:a 1, :b 2, :c 3, :d 4, :m 666, :n 777}
(merge {:a 1 :b 2} {:a 3 :c 4})
;=> {:a 3, :b 2, :c 4}
(merge-with + {:a 1 :b 2} {:a 3 :c 4})
;=> {:a 4, :b 2, :c 4}

关于嵌套映射的使用。想要更改嵌套映射中的值需要先进入想要的位置,创建一个更改后的映射,并用 assoc 将更改的信息关联到这个中间映射,并一路返回到根。Clojure提供三个简化嵌套更新的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(def users {:kyle {:date-joined "2009-01-01"
:summary {:average {:monthly 1000
:yearly 12000}}}})

; assoc-in 可以设置新值,若不存在任何嵌套映射,在创建并正确关联。
(assoc-in users [:kyle :summary :average :monthly] 3000)
;=> {:kyle {:date-joined "2009-01-01", :summary {:average {:monthly 3000,
; :yearly 12000}}}}

; get-in 可以嵌套读取值
(get-in users [:kyle :summary :average :monthly])
;=> 1000

; update-in 可以更新嵌套映射中的值,其不提供新值,而是提供一个函数
(update-in users [:kyle :summary :average :monthly] + 500)
;=> {:kyle {:date-joined "2009-01-01", :summary {:average {:monthly 1500,
; :yearly 12000}}}}

keys将所有的键作为序列返回,vals则将所有的值作为序列返回。

1
2
3
4
(keys {:sundance "spaniel", :darwin "beagle"})
;-> (:sundance :darwin)
(vals {:sundance "spaniel", :darwin "beagle"})
;-> ("spaniel" "beagle")

无法确认究竟是键对应的值为 nil,还是这个键在映射表中根本就不存在。contains?函数就可以解决这个问题。((:z the-map 26) 这种形式也可以做到。)

1
2
3
(def score {:stu nil :joey 100})
(contains? score :stu)
;-> true

8. Set

主要函数:conjdisjcontains?sethash-setsorted-setunionintersectiondifferenceselect

Clojure set 工作方式和数学中的 set 是一样的,是一种集合,其中的元素无序且唯一。Clojure 支持两种不同的 set:排序的(sorted-set)和不排序的(hash-set)。sorted-set 会依据自然顺序对值进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#{:a :b :c}
;=> #{:c :b :a}
(conj #{:a :b :c} :d)
;=> #{:c :b :d :a}
(conj #{:a :b :c} :a)
;=> #{:c :b :a}
(disj #{:a :b :c} :a)
;=> #{:c :b}
(contains? #{1 2 3} 3)
;=> true

; set函数期望其第一个参数是个容器。而hash-set则接受可变的参数列表。
(set [:a :b :c])
;=> #{:c :b :a}
(hash-set 1 2 3)
;=> #{1 3 2}
(sorted-set 2 3 1)
;=> #{1 2 3}

clojure.set 集合函数

需先调用(use 'clojure.set)

union返回的集合,包含了所有输入集合中的元素。
intersection返回的集合,其所有元素都曾同时出现于多个输入集合中。
difference 返回的集合,其所有元素都出现于第一个输入集合,但却未出现于第二个中。
select返回所有元素都能与给定谓词相匹配的一个集合。

1
2
3
4
5
6
7
8
9
10
11
12
(def languages #{"java" "c" "d" "clojure"})
(def beverages #{"java" "chai" "pop"})

(use 'clojure.set)
(union languages beverages)
;=> #{"java" "c" "d" "clojure" "chai" "pop"}
(difference languages beverages)
;=> #{"c" "d" "clojure"}
(intersection languages beverages)
;=> #{"java"}
(select #(= 1 (.length %)) languages)
;=> #{"c" "d"}

差集和并集除了是集合论的一部分,也是关系代数的一部分,下面介绍了投影、笛卡尔积、连接、重命名等方法。

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
; 音乐作品集内存数据库示例
(def compositions
#{{:name "The Art of the Fugue" :composer "J. S. Bach"}
{:name "Musical Offering" :composer "J. S. Bach"}
{:name "Requiem" :composer "Giuseppe Verdi"}
{:name "Requiem" :composer "W. A. Mozart"}})
(def composers
#{{:composer "J. S. Bach" :country "Germany"}
{:composer "W. A. Mozart" :country "Austria"}
{:composer "Giuseppe Verdi" :country "Italy"}})
(def nations
#{{:nation "Germany" :language "German"}
{:nation "Austria" :language "German"}
{:nation "Italy" :language "Italian"}})

(select #(= (:name %) "Requiem") compositions)
;=> #{{:name "Requiem", :composer "W. A. Mozart"}
; {:name "Requiem", :composer "Giuseppe Verdi"}}

; project函数返回的那些映射表中,仅包含与参数匹配的键。 (project relation keys)
(project compositions [:name])
;=> #{{:name "Musical Offering"}
; {:name "Requiem"}
; {:name "The Art of the Fugue"}}

; 笛卡尔积
(for [m compositions c composers] (concat m c))

; join 集合连接
(join compositions composers)
;=> #{{:composer "W. A. Mozart", :country "Austria", :name "Requiem"}
; {:composer "J. S. Bach", :country "Germany", :name "Musical Offering"}
; {:composer "Giuseppe Verdi", :country "Italy", :name "Requiem"}
; {:composer "J. S. Bach", :country "Germany", :name "The Art of the Fugue"}}

;如果两个关系中的键名不匹配,你可以传入一个keymap,将relation-1中的键名映射到relation-2中对应的键。
;例如,你可以将使用:country的composers,与使用:nation的nations相连接。
(join composers nations {:country :nation})
;=> #{{:composer "W. A. Mozart", :country "Austria", :nation "Austria", :language "German"}
; {:composer "J. S. Bach", :country "Germany", :nation "Germany", :language "German"}
; {:composer "Giuseppe Verdi", :country "Italy", :nation "Italy", :language "Italian"}}


; 示例: 所有创作了安魂曲的作曲家们,家乡都在哪些国家
(project
(join
(select #(= (:name %) "Requiem") compositions)
composers)
[:country])
;=> #{{:country "Italy"} {:country "Austria"}}

; rename 可以给键(数据库的列)重命名
(rename compositions {:name :title})
;=>#{{:composer "Giuseppe Verdi", :title "Requiem"}
; {:composer "W. A. Mozart", :title "Requiem"}
; {:composer "J. S. Bach", :title "The Art of the Fugue"}
; {:composer "J. S. Bach", :title "Musical Offering"}}

9. 序列

序列是一个接口(ISeq),Clojure 数据结构、函数和宏都普遍实现这个接口,序列抽象使所有数据结构的外表和行为像列表一样。

first返回序列的第一个元素,rest返回排除第一个元素的序列,但是对所有集合类型采取相同的方式。

cons会在序列的开始位置(即使是向量也一样)添加一个元素。

序列抽象通常是惰性的,尽管 first、rest、cons的结果打印出来像一个列表,但它们并没有进行创建列表的额外工作。序列抽象使一切都像操纵真正的列表一样,但是避免真正地创建任何新数据结构或者进行任何不必要的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(first (list 1 2 3))
;=> 1
(rest (list 1 2 3))
;=> (2 3)
(first [1 2 3])
;=> 1
(rest [1 2 3])
;=> (2 3)
(first {:a 1 :b 2}) ; 不保证项的顺序
;=> [:b 2]
(rest {:a 1 :b 2})
;=> ([:a 1])
(first []) ; 空集合调用first返回nil
;=> nil
(rest []) ; 空集合调用 rest 返回空序列
;=> ()

(cons 1 [2 3 4 5])
;=> (1 2 3 4 5)

(list? (cons 1 (list 2 3)))
;=> false

一切皆序列

主要函数:firstrestconsseqnext

可被视为序列的容器,被称为可序化的,可序化的容器包括:所有的Clojure容器、所有的Java容器、Java数组和字符串、正则表达式的匹配结果、目录结构、输入/输出流、XML树。

除了序列三大核心firstrestcons,还有seq,seq 函数会返回一个序列,该序列源自任何一个可序化的其他容器。next 函数也会返回一个序列,该序列由除第一个元素以外的其他所有元素组成。(next aseq)等价于 (seq (rest aseq))

1
2
3
4
5
6
7
8
9
10
(seq nil)
;=> nil
(seq ())
;=> nil
(rest ())
;=> ()
(next ())
;=> nil
(seq (rest ()))
;=> nil

conj & into

conj 会向容器添加一个或是多个元素,into 则会把容器中的所有元素添加至另一个容器。这两个方法的返回值类型不是序列,而是容器类型。
添加数据时,conj和into都会根据底层数据结构的特点选取最高效的插入点。

1
2
3
4
5
6
7
8
9
10
11
;对于列表而言,conj和into会在其前端进行添加。
(conj '(1 2 3) :a)
;=> (:a 1 2 3)
(into '(1 2 3) '(:a :b :c))
;=> (:c :b :a 1 2 3)

;而对于向量,conj和into则会把元素添加至末尾。
(conj [1 2 3] :a)
;=> [1 2 3 :a]
(into [1 2 3] [:a :b :c])
;=> [1 2 3 :a :b :c]

创建序列

主要函数:rangerepeattakeiteratecycleinterleaveinterpose

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
;ranage会生成一个从start开始到end结束的序列,每次的增量为step。 (range start? end step?)
(range 10)
;=> (0 1 2 3 4 5 6 7 8 9)
(range 10 20)
;=> (10 11 12 13 14 15 16 17 18 19)
(range 1 25 2)
;=> (1 3 5 7 9 11 13 15 17 19 21 23)

;repeat函数会重复n次元素x。 (repeat n x)
(repeat 5 1)
;=> (1 1 1 1 1)
(repeat 10 "x")
;=> ("x" "x" "x" "x" "x" "x" "x" "x" "x" "x")

;(iterate f x) iterate起始于值x,并持续地对每个值应用函数f,以计算下一个值,直至永远。
;由于这是个无限序列,你需要另一个新函数 (take n sequence)。take会返回一个包含了容器中前n项元素的惰性序列。
(take 10 (iterate inc 1))
;=> (1 2 3 4 5 6 7 8 9 10)

;cycle函数接受一个容器,并无限的对其进行循环。 (cycle coll)
(take 10 (cycle (range 3)))
;=> (0 1 2 0 1 2 0 1 2 0)

;interleave函数接受多个容器作为参数,并产生一个新的容器,这个新容器会从每个参数容器中交错地提取元素,直至其中某个容器元素被耗尽。
(defn whole-numbers [] (iterate inc 1))
(interleave (whole-numbers) ["A" "B" "C" "D" "E"])

;interpose函数,把输入序列中的每个元素用分隔符隔开,并作为新的序列返回。(interpose separator coll)
(interpose "," ["apples" "bananas" "grapes"])
;=> ("apples" "," "bananas" "," "grapes")
;(apply f args* argseq)apply函数接受一个函数f、一些可选的args和一个序列argseq作为参数。之后会调用 f,并将args和argseq解开为一个参数列表传给f。
(apply str (interpose \, ["apples" "bananas" "grapes"]))
;=> "apples,bananas,grapes"
;(join separator sequence)
(use '[clojure.string :only (join)])
(join \, ["apples" "bananas" "grapes"])
;=> "apples,bananas,grapes"

;对应每种Clojure中的容器类型,都有一个可以接受任意数量参数的函数,用来创建该类型的容器。
;hash-set与set与其工作方式稍有不同:set函数期望其第一个参数是个容器。而hash-set则接受可变的参数列表。
(set [1 2 3])
;=> #{1 3 2}
(hash-set 1 2 3)
;=> #{1 3 2}

;vector也有一个近亲vec,vec接受容器作为参数,而非可变的参数列表。
(vec (range 3))
;=> [0 1 2]
(vector 0 1 2)
;=> [0 1 2]

过滤序列

主要函数:filtertake-whiledrop-whilesplit-atsplit-with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(defn whole-numbers [] (iterate inc 1))
;(filter pred coll) filter接受一个谓词和一个容器作为参数,并返回一个序列,这个序列的所有元素都经谓词判定为真。
(take 10 (filter even? (whole-numbers)))
;=> (2 4 6 8 10 12 14 16 18 20)

;使用take-while从序列中截取开头的一段,其每个元素都被谓词判定为真。 (take-while pred coll)
;字符串中逐个获取第一个元音字符之前的所有非元音字符
(take-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox")
;=> (\t \h)
;1、集合同时也可作为函数。所以你可以把#{\a\e\i\o\u}读作“元音集”,或是“用于检测参数是否为元音的函数。”
;2、complement 会反转另一个函数的行为。前例的那个反转函数用于检测参数是不是一个元音。

;与take-while相对的是drop-while函数。drop-while 从序列的起始位置开始,逐个丢弃元素,直至谓词判定为真,然后返回序列剩余的部分。
(drop-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox")
;=> (\e \- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)

;split-at和split-with能把一个容器一分为二。(split-at index coll) (split-with pred coll)
;split-at接受一个索引作为参数,而split-with则接受一个谓词。
(split-at 5 (range 10))
;=>[(0 1 2 3 4) (5 6 7 8 9)]
(split-with #(<= % 10) (range 0 20 2))
;=>[(0 2 4 6 8 10) (12 14 16 18)]

序列谓词

主要函数:every?somenot-every?not-any?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;every?要求其他谓词对序列中的每个元素都必须判定为真。
(every? odd? [1 3 5])
;=> true
(every? odd? [1 3 5 8])
;=> false
;(some pred coll) 只要有一个元素被谓词判定为非假,some就会返回这个值,如果没有任何元素符合,则some返回nil。
(some even? [1 2 3])
-> true
(some even? [1 3 5])
-> nil
;some 返回的是第一个符合项的值,而非 true。
(some identity [nil false 1 nil 2])
;=> 1
(not-every? even? (whole-numbers))
;=> true
(not-any? even? (whole-numbers))
;=> false

序列转换

主要函数:mapreducesortsort-byfor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;映射函数map (map f coll) map接受一个源容器coll和一个函数f作为参数,并返回一个新的序列。
(map #(format "<p>%s</p>" %) ["the" "quick" "brown" "fox"])
;=> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")
;还可以传入多个容器给map。在这种情况下,f必须是一个多参函数。map会从每个容器分别取出一个值,作为参数来调用f,直到数量最少的那个容器被耗尽为止。
(map #(format "<%s>%s</%s>" %1 %2 %1)
["h1" "h2" "h3" "h1"] ["the" "quick" "brown" "fox"])
;=> ("<h1>the</h1>" "<h2>quick</h2>" "<h3>brown</h3>" "<h1>fox</h1>")

;归纳函数reduce (reduce f coll) reduce首先用coll的前两个元素作为参数来调用f,然后用得到的结果和第三个元素作为参数,继续调用f。
(reduce + (range 1 11))
;=> 55
(reduce * (range 1 11))
;=> 3628800

;(sort comp? coll) (sort-by a-fn comp? coll) sort 会依据元素的自然顺序对容器进行排序,sort-by 则会对每个元素调用 a-fn,再依据得到的结果序列来进行排序。
(sort [42 1 7 11])
;=> (1 7 11 42)
(sort-by #(.toString %) [42 1 7 11])
;=>` (1 11 42 7)
;可以为sort或sort-by指定一个可选的比较函数comp。
(sort > [42 1 7 11])
;=> (42 11 7 1)
(sort-by :grade > [{:grade 83} {:grade 90} {:grade 77}])
;=> ({:grade 90} {:grade 83} {:grade 77})

Clojure把列表解析的概念泛化为了序列解析(sequence comprehension)。在Clojure中,是使用for宏来进行解析的。列表解析比诸如 map 和filter 这样的函数更加通用,而且,事实上它可以模拟之前的大多数过滤和转换函数。

1
(for [binding-form coll-expr filter-expr? ...] expr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(defn whole-numbers [] (iterate inc 1))
(for [word ["the" "quick" "brown" "fox"]] (format "<p>%s</p>" word))
;=> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")

;借助:when子句,解析也可以用来模拟filter函数。
(take 10 (for [n (whole-numbers) :when (even? n)] n))
;=> (2 4 6 8 10 12 14 16 18 20)

;只要:while字句的表达式保持为真,它就会继续进行求值。
(for [n (whole-numbers) :while (even? n)] n)
;=> ()
(for [n (whole-numbers) :while (odd? n)] n)
;=> (1)
(for [n (whole-numbers) :while (< n 5)] n)
;=> (1 2 3 4)

;多个绑定表达式
(for [file "ABCDEFGH" rank (range 1 9)] (format "%c%d" file rank))
;=> ("A1" "A2" ...已省略... "H7 ""H8")
(for [rank (range 1 9) file "ABCDEFGH"] (format "%c%d" file rank))
;=> ("A1" "B1"... "G8" "H8")

序化正则表达式、文件系统、流

  • 正则表达式:
1
2
3
4
5
6
7
8
9
10
(def matcher (re-matcher #"\d+" "abc12345def678"))
;=>#'user/matcher
(re-find matcher)
;=>"12345"
(re-find matcher)
;=> "678"

;; If you only want the first match, it is shorter to call re-find with the pattern and the string to search, rather than explicitly creating a matcher as above.
(re-find #"\d+" "abc12345def")
;=>"12345"

使用正则更好的做法:(re-seq regexp string) re-seq 会把匹配结果暴露为一个不可变的序列。

1
2
3
4
5
6
7
8
(re-seq #"\w+" "the quick brown fox")
;=> ("the" "quick" "brown" "fox")
(sort (re-seq #"\w+" "the quick brown fox"))
;=> ("brown" "fox" "quick" "the")
(drop 2 (re-seq #"\w+" "the quick brown fox"))
;=> ("brown" "fox")
(map #(.toUpperCase %) (re-seq #"\w+" "the quick brown fox"))
;=> ("THE" "QUICK" "BROWN" "FOX")
  • 文件系统:
1
2
3
4
5
6
7
8
9
10
(import '(java.io File))
; 返回的是File数组,而非序列
(.listFiles (File. "."))
; 返回序列
(seq (.listFiles (File. ".")) )
; 想要获取name时可以使用map,一旦你决定使用诸如map这样的函数,再调用seq就会显得多余。序列库中的函数会替你调用seq。
(map #(.getName %) (.listFiles (File. ".")))

;遍历整个目录树。Clojure通过file-seq提供了一个深度优先的遍历方式
(file-seq (File. "."))
  • 流:
1
2
3
4
5
6
7
(use '[clojure.java.io :only (reader)])
; 统计文件多少行
(with-open [rdr (reader "src/examples/java.clj")]
(count (line-seq rdr)))
; 仅对非空行计数
(with-open [rdr (reader "src/examples/utils.clj")]
(count (filter #(re-find #"\S" %) (line-seq rdr))))
  • 综合示例:获取clj代码行数
1
2
3
4
5
6
7
8
9
10
11
(use '[clojure.java.io :only (reader)])
(defn non-blank? [line] (if (re-find #"\S" line) true false))
(defn non-svn? [file] (not (.contains (.toString file) ".svn")))
(defn clojure-source? [file] (.endsWith (.toString file) ".clj"))
(defn clojure-loc [base-file]
(reduce
+
(for [file (file-seq base-file)
    :when (and (clojure-source? file) (non-svn? file))]
(with-open [rdr (reader file)]
    (count (filter non-blank? (line-seq rdr)))))))
CATALOG
  1. 1. Clojure 概述
    1. 1.1. 1. Clojure 是 Lisp 的一个变种
    2. 1.2. 2. Clojure 以JVM为宿主
    3. 1.3. 3. Clojure 是一种函数式编程语言
  2. 2. Clojure 基础
    1. 2.1. 前期准备
      1. 2.1.1. 1. Clojure REPL(读取 - 求值 - 打印循环)
      2. 2.1.2. 2. 特殊 REPL 变量
      3. 2.1.3. 3. 文档查找
      4. 2.1.4. 4. 其他细节
    2. 2.2. Clojure 数据结构
      1. 2.2.1. 1. nil / 真值 / 价值
      2. 2.2.2. 2. 字符 / 字符串
      3. 2.2.3. 3. 数值
      4. 2.2.4. 4. 符号 / 关键字
      5. 2.2.5. 5. 列表
      6. 2.2.6. 6. 向量
      7. 2.2.7. 7. 映射
      8. 2.2.8. 8. Set
        1. 2.2.8.1. clojure.set 集合函数
      9. 2.2.9. 9. 序列
        1. 2.2.9.1. 一切皆序列
        2. 2.2.9.2. conj & into
        3. 2.2.9.3. 创建序列
        4. 2.2.9.4. 过滤序列
        5. 2.2.9.5. 序列谓词
        6. 2.2.9.6. 序列转换
        7. 2.2.9.7. 序化正则表达式、文件系统、流