Clojure 概述
关键词:
JVM
、Lisp
、动态类型
、函数式语言
、不可变数据结构
、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 | (+ 1 2) |
第二个表达式定义了一个命名空间限定的全局符号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 | (type \a) |
Clojure 字符串是 Java 字符串,使用双引号表示。(单引号则是另一个读取器宏,来表示 Symbol)
1 | (type "hello") |
Java String API 在 Clojure 中依然很有用。
3. 数值
Clojure 使用的整数是64位整数(Long
),浮点数是64位浮点数(Double
)。当需要更大的范围时,可以使用BigInteger
,BigDecimal
。此外,还有一个不常见的数值类型:比例(ratio
),比例在两个整数相除时创建。
1 | (type 2) |
当不同数值类型在同一个算术运算中混合使用时,具有高传染性的数值类型将其类型传染给结果(long < bigint < ratio < bigdec < double)
1 | (+ 1 1N) |
溢出(overflow):在 Clojure 中可能产生溢出的算术运算只有整数加法、减法和乘法(整数相除时,如果超过范围则生成一个比例)。
溢出发生时 Clojure 会抛出 ArtithmeticException
异常。如果希望结果提升为大整数,则应该使用:+'
,-'
,*'
,inc'
,dec'
。
1 | (inc 9223372036854775807) |
4. 符号 / 关键字
符号
是 Clojure 中的标识符,代表值的名称。符号本身只包含可选命名空间的名称,但当一个表达式求值时,它们被所代表的值取代。
在一个程序中,符号通常被解析为不是符号的其他内容,但是可以通过一个前导的单引号引用符号
,将其当成一个值而非标识符。
当为一个符号加上引号,就将这个引号当成数据而不是代码来处理,在实践中一般不会这么做,因为有另一种特殊类型:关键词
,关键词从不引用其他值,求值的结果总是他们本身。关键词的典型用法是作为哈希映射中的键和枚举值。
5. 列表
主要函数:
list
,list?
,conj
,peek
,pop
,count
Clojure 列表是单链表。只能从列表前端添加或删除元素,这意味着多个不同列表可以共享相同尾部,使列表成为最简单的不可变数据结构。
1 | (list 1 2 3 4 5) |
列表的特殊性:Clojure 会假定列表中出现的第一个符号表示函数(或者宏)名称。Clojure 视图以处理所有列表的相同方式来处理表(1,2,3) ,第一个元素被视为函数,而这里的整数 1 并不是函数。若希望将其作为数据而非代码,解决方式就是加上引号:'(1,2,3)
。
实践中一般不会在 Clojure 代码中使用列表作为数据,而是使用向量类型。
6. 向量
主要函数:
vector
,get
,nth
,assoc
,conj
,peek
,pop
,count
,subvec
,函数本身
向量可以使用vector
函数创建,也可以使用方括号表示法创建,可以快速随机访问向量中的元素。获取其中元素中的方法有get
和nth
,差别在于没有找到相应值时,nth 会报出错误,get 返回 nil。
1 | (vector 10 20 30 40 50) |
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 |
|
conj
函数同样适用于向量,但在向量中新元素将会被添加到最后,因为那是向量中最快速的插入位置。
1 | (conj [1 2 3 4 5] 6) |
peek
和pop
也适用于向量,方法会查看向量的尾部,而不是列表的表头。
1 | (peek [1 2]) |
向量本身也是取单一参数的函数。(但向量函数不接受第二个参数)
1 | (the-vector 3) |
subvec
会返回向量的一个子向量 (subvec avec start end?)
。若未指定end,则默认为向量的末尾。
1 | (subvec [1 2 3 4 5] 3) |
7. 映射
主要函数:
hash-map
,sorted-map
,assoc
,dissoc
,select-keys
,merge
,merge-with
,assoc-in
,get-in
,update-in
,keys
,vals
,contains?
,get
,函数本身
一个映射就是一个键值对序列。映射可以使用hash-map
函数构建。依据键获取对应的值时除了使用get
函数外,映射本身也是一个函数。
1 | (def the-map {:a 1 :b 2 :c 3}) |
映射字面量和 hash-map 函数不完全等价,Clojure 实际上有哈希映射(hash-map)
以及数组映射(array-map)
。数组映射以有序方式保存键和值,以扫描的方式进行查找。如果使用 assoc 函数将太多键关联到一个数组映射,那将会得到一个哈希映射(而哈希映射变得太小不会反悔一个数组映射)。透明地替换数据结构的实现是 Clojure 提高性能的常用技巧。
此外,sorted-map
不会按照存放的顺序返回,它会根据键来进行排序。
映射的修改方法有assoc
和dissoc
等。assoc
返回新增了一个键值对的映射表, dissoc
返回移除了某些键的映射表。 select-keys
返回一个映射表,仅保留了参数传入的那些键。merge
可以合并映射表。如果多个映射表包含了同一个键,那么最右边的获胜。merge-with
与 merge 很类似,除了当两个或以上的映射表中有相同的键时,你能指定一个你自己的函数,来决定如何合并这个键对应的值。
1 | (def updated-map (assoc the-map :d 4)) |
关于嵌套映射的使用。想要更改嵌套映射中的值需要先进入想要的位置,创建一个更改后的映射,并用 assoc 将更改的信息关联到这个中间映射,并一路返回到根。Clojure提供三个简化嵌套更新的函数。
1 | (def users {:kyle {:date-joined "2009-01-01" |
keys
将所有的键作为序列返回,vals
则将所有的值作为序列返回。
1 | (keys {:sundance "spaniel", :darwin "beagle"}) |
无法确认究竟是键对应的值为 nil,还是这个键在映射表中根本就不存在。contains?函数就可以解决这个问题。((:z the-map 26)
这种形式也可以做到。)
1 | (def score {:stu nil :joey 100}) |
8. Set
主要函数:
conj
,disj
,contains?
,set
,hash-set
,sorted-set
,union
,intersection
,difference
,select
Clojure set 工作方式和数学中的 set 是一样的,是一种集合,其中的元素无序且唯一。Clojure 支持两种不同的 set:排序的(sorted-set)和不排序的(hash-set)。sorted-set 会依据自然顺序对值进行排序。
1 | #{:a :b :c} |
clojure.set 集合函数
需先调用(use 'clojure.set)
。
union
返回的集合,包含了所有输入集合中的元素。intersection
返回的集合,其所有元素都曾同时出现于多个输入集合中。difference
返回的集合,其所有元素都出现于第一个输入集合,但却未出现于第二个中。select
返回所有元素都能与给定谓词相匹配的一个集合。
1 | (def languages #{"java" "c" "d" "clojure"}) |
差集和并集除了是集合论的一部分,也是关系代数的一部分,下面介绍了投影、笛卡尔积、连接、重命名等方法。
1 | ; 音乐作品集内存数据库示例 |
9. 序列
序列是一个接口(ISeq
),Clojure 数据结构、函数和宏都普遍实现这个接口,序列抽象使所有数据结构的外表和行为像列表一样。
first
返回序列的第一个元素,rest
返回排除第一个元素的序列,但是对所有集合类型采取相同的方式。
cons
会在序列的开始位置(即使是向量也一样)添加一个元素。
序列抽象通常是惰性的,尽管 first、rest、cons的结果打印出来像一个列表,但它们并没有进行创建列表的额外工作。序列抽象使一切都像操纵真正的列表一样,但是避免真正地创建任何新数据结构或者进行任何不必要的工作。
1 | (first (list 1 2 3)) |
一切皆序列
主要函数:
first
,rest
,cons
,seq
,next
可被视为序列的容器,被称为可序化的,可序化的容器包括:所有的Clojure容器、所有的Java容器、Java数组和字符串、正则表达式的匹配结果、目录结构、输入/输出流、XML树。
除了序列三大核心first
、rest
、cons
,还有seq
,seq 函数会返回一个序列,该序列源自任何一个可序化的其他容器。next
函数也会返回一个序列,该序列由除第一个元素以外的其他所有元素组成。(next aseq)
等价于 (seq (rest aseq))
。
1 | (seq nil) |
conj & into
conj
会向容器添加一个或是多个元素,into
则会把容器中的所有元素添加至另一个容器。这两个方法的返回值类型不是序列,而是容器类型。
添加数据时,conj和into都会根据底层数据结构的特点选取最高效的插入点。
1 | ;对于列表而言,conj和into会在其前端进行添加。 |
创建序列
主要函数:
range
,repeat
,take
,iterate
,cycle
,interleave
,interpose
1 | ;ranage会生成一个从start开始到end结束的序列,每次的增量为step。 (range start? end step?) |
过滤序列
主要函数:
filter
,take-while
,drop-while
,split-at
,split-with
1 | (defn whole-numbers [] (iterate inc 1)) |
序列谓词
主要函数:
every?
,some
,not-every?
,not-any?
1 | ;every?要求其他谓词对序列中的每个元素都必须判定为真。 |
序列转换
主要函数:
map
,reduce
,sort
,sort-by
,for
1 | ;映射函数map (map f coll) map接受一个源容器coll和一个函数f作为参数,并返回一个新的序列。 |
Clojure把列表解析的概念泛化为了序列解析(sequence comprehension)。在Clojure中,是使用for宏来进行解析的。列表解析比诸如 map 和filter 这样的函数更加通用,而且,事实上它可以模拟之前的大多数过滤和转换函数。
1 | (for [binding-form coll-expr filter-expr? ...] expr) |
1 | (defn whole-numbers [] (iterate inc 1)) |
序化正则表达式、文件系统、流
- 正则表达式:
1 | (def matcher (re-matcher #"\d+" "abc12345def678")) |
使用正则更好的做法:(re-seq regexp string)
re-seq 会把匹配结果暴露为一个不可变的序列。
1 | (re-seq #"\w+" "the quick brown fox") |
- 文件系统:
1 | (import '(java.io File)) |
- 流:
1 | (use '[clojure.java.io :only (reader)]) |
- 综合示例:获取clj代码行数
1 | (use '[clojure.java.io :only (reader)]) |