背景简述
本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Clojure语言的。坊间传闻:通常情况下,最好是有一定Java的开发工作经验,再转CLojure可能容易一些。我入职后的实际经历也确实让我感受到了Clojure的自学难度略大于自学Java,遇到的困难主要与中文资料较少有关,具体为:
1 中文的面向新手的较为系统的教程材料较少,目前个人感觉最好用的还是《CLojure编程 Emerick著》这本书,网上应该很好找,如果大家没有电子版的话可以留言,我看到后就立刻分享给大家
2 中文的网上相关问题和讨论较少, 以前学Java的时候基本遇到的问题用百度就能解决,现在大概率要直接用bing或谷歌,或者直接在*(虽然是英文的,但貌似是最好用的IT问答网站)上查
我的这个系列笔记主要是基于 0工作经验的后端开发转学Clojure 的场景下完成的,里面有一些个人观点和个人理解的注释,写的时候是为了便于自己理解相关的概念,现在分享出来一方面是希望能帮助像我一样的新手更好地理解,另一方面也是希望有高手能够发现错误并帮忙斧正,谢谢
一些格式的简单约定:
粗体:比较重要的内容
斜体:我个人理解/观点或是补充内容,大家选择性食用
P15:表示书上第15页
第3章 集合类与数据结构
四种基本数据结构:list:'( );vector:[ ];map { }:;set:#{ }
3.1 抽象优于实现
Clojure的立场:100个函数操作1种数据结构比10个函数操作10种数据结构要好,Clojure的“抽象优于实现”足以跟Java的接口相媲美,但某些微妙的地方使得Clojure对抽象的强调更彻底,能产生更大的威力
下面将介绍Clojure集合中实现的几个主要抽象:
3.1.1 Collection
Collection(集合),所有数据结构都实现了Collection抽象
个人理解Collection抽象就是所有数据结构的基础,这一部分了解即可,主要是要懂得具体实现的数据结构的使用
核心函数:
conj:[coll value],添加元素到集合,添加列表、vector时是加到头部,添加向量时是加到尾部,添加map和set时顺序不一定
seq:获取集合的顺序视图
count:获取集合的元素个数
empty:获取跟所提供集合类型相同的空集合
=:判断两个或多个集合是否相等,这个相等时判断引用相等,使用的是java的equals(地址或被被重写的判断逻辑)定义
3.1.2 Sequence
序列,定义了一个获取并且遍历各种集合的一个顺序视图的一种方法
核心函数:
seq:返回传入参数的一个序列
first、rest、next、last:遍历序列的一些方法,rest和next基本一样,只是返回结果为空时,rest返回空序列,next返回nil
lazy-seq:创建一个内容为表达式结果的惰性序列
可以用seq来输出结果的类型被称为可序列的类型:
所有Clojure集合
所有Java集合
所有Java map
所有java.lang.CharSequence,包括String
实现了java.lang.Iterablede的任意类型
数组
nil
实现了clojure.lang.Seqable接口的类型
注意:序列不是列表,序列是惰性的,计算长度要遍历,而列表不需要
创建序列有三种办法:其他集合调用seq、利用cons、利用list*
cons:[value coll],始终把第一个参数加到第二个参数的头上,而不管第二个参数具体的集合类型,和conj不同
惰性序列:通过lazy-seq创建惰性序列,这个宏接受任意返回值是一个可序列的值的表达式
惰性序列使用的注意点:
- 定义惰性序列的代码尽量不要有副作
- 避免头保持,惰性序列一个元素被实例化之后,只要保持了对这个序列的引用,那么这个元素就会一直保持着了,这意味着只要保持引用,那么序列的元素就不能被垃圾回收,会影响性能,示例见P97
3.1.3 Associative
关系型数据结构Associative接口所抽象的就是把key和value关联起来的结构,比如map
核心函数:
assoc:向集合添加一个新的key-value映射,也可以操作vector,不过要指定插入的坐标
assoc-in:嵌套解构赋值
dissoc:移除一个key-value映射
get:找出指定key的value,可以操作set,如果存在则返回值,不存在返回nil
find:和get类似,只不过返回的不是value,而是key-value,这样就能判断一个map中是否真实存在一个键值对了
contains?:判断是否包含这个key,也可以用在vector中,不过key相当于vector的index
3.1.4 Indexed
索引集合,一般来说,代码是不需要依赖下标的,所以vector部分没有考虑下标的问题
核心函数:
nth:只能接受数字作为查询值,根据下标返回值,如果越界会抛出异常,对错误的容忍度较低
3.1.5 Stack
栈,后进先出的集合,列表和vector都可以当作栈来用
核心函数:
conj:入栈
pop:获取栈顶的值,并移除这个值
peek:获取栈顶的值,但是不移除
3.1.6 Set
退化的map,只能存非重复元素
核心函数:
get:获取值,但是可以指定获取失败的返回值,P104
disj:移除元素
3.1.7 Sorted
有序集合,顺序可以通过comparator接口来定义,只有map和set实现了sorted接口
核心方法:
rseq:反序返回一个集合的元素
subseq:返回一个集合的某一区间的元素的序列
rsubseq:反序返回区间序列
sorted-map或sorted-set:创建有序map或set,见P104
compare方法是按照字典排序法进行排序的,返回值为-1,0,1;
使用比较器和谓词来定义排序规则:
Clojure的所有函数都实现java.util.Comparator接口
任何一个两参谓词都可以实现比较器
sorted-map和sorted-set是通过compare来定义默认规则进行排序的map和set,而sorted-map-by和sorted-set-by是接受一个比较器(任意两参谓语也可以)来定义排序规则
3.2 访问集合元素的简洁方式
集合本身也是函数,并不是必须要写get、nth
集合的key通常也是函数,比如实际开发工作中,便捷的map取值就是最常用到的,(:id {:id "123" :name "arvin"})
3.2.1 习惯用法
通常推荐吧关键字或者符号作为查找函数来使用,这样可以避免NPE,但如果一个集合的key不是关键字或符号类型,那么只能使用集合本身或者get、nth作为查找函数了,具体见P111
3.2.2 集合、key以及高阶函数
some非常适合在条件判断中判断集合是否包含某个元素,更通用的是filter,它返回一个惰性序列
举例:
(some #{1 2 3} [2 3 4 5])
;= 3
3.3 数据结构的类型
Clojure提供了一些具体的数据结构可以使用,这些具体的结构都实现了一个或多个抽象,它们的行为和语义都是由那些他们实现的抽象所定义的
个人认为这一部分是重点要掌握的内容,因为在实际的开发工作中用的最多,尤其是map相关的内容
3.3.1 List
大多数时候用来做函数调用,将其作为存储数据的场景用的不多
结构是一个单向链表,所以seq一个列表是会返回它本身,而不是顺序视图
真正存储数据的列表需要加一个' ,否则其实会被当作函数调用来使用,但是只要有了',那么括号里面的所有内容都不会被求值,比如'(1 2 (+1 3))的输出结果还是(1 2 (+1 3)),所以这个时候要用(list 1 2 (+ 1 3))
可以通过list?判断一个集合是不是列表
3.3.2 vector
是一种顺序数据结构,支持高校随机访问和更改语义,类似于Java的ArrayList,实现了associative、indexed、stack抽象
可以通过[] 字面量、vector、vec来创建
元组(tuple)是vector最常见的使用场景,任何时候想把多个值绑定在一起处理,比如从一个函数中返回多个值,那么就可以把多个值放入一个vector,其实是多个返回值放进一个vector中,见p114
3.3.3 set
就是具体的set,没什么特别要注意的
3.3.4 map
有方法keys和vals,分别返回所有的键和所有的值
map也被用来保存总结信息、索引信息或对应关系,比如group-by
关于:keys的使用虽然在let解构里面重点讲了,但是在这里再提醒一下,因为实际开发中用到的确实非常多
举例:
(def chas {:name "arvin" :age 20 :location "SH"}
(let [{:keys [name age]} chas]
(str name " is " age " years old."))
3.4 不可变性和持久性
集合的重要特征是不可变和持久的
3.4.1 持久性和结构共享
对Clojure不可变数据结构的操作是非常高效的,因为Clojure数据结构是持久的,从而保证修改过的集合和原来的集合共享数据存储,而且保证了两个集合一样的高效率
持久性:不是值序列化、存储等含义,而是一种能够使不可变数据结构的所有修改版本的效率一直保持一致的一种技术,这个技术是通过结构共享实现的
这里的持久性跟我们在数据库里面理解的持久性不一样,要注意区分
3.5 元数据
有关数据的数据是元数据,主要表现形式有:
- 类型生命和控制修饰符
- Java注解是类、方法及方法参数等的元数据
Clojure的元数据可以被关联到任何数据结构,而且始终是一个map的形式
^ 字面量:就是把元数据关联到值的方式
通过(meta var)可以查看元数据
with-meta和vary-meta可以更新元数据
元数据是不会因为数据结构的修改而修改的,因此关于一个结构的定义是能够一直被保存下来的