Clojure 学习笔记 :2 你好,集合

Clojure 零基础 学习笔记 数据结构 集合


It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. --- Alan J. Perlis
100 个函数操作 1 种数据结构,比 10 个函数操作 10 种数据结构要好 --- Alan J. Perlis

Clojure 提供的基本数据结构[1]

  • list
  • vector
  • map
  • set

而上述 4 种数据结构我们都称之为集合(collection)
集合就是把一些东西放在一起,打个包裹,方便管理。

它们各有特色:

  • list 是 Clojure 中最为简单的数据结构
  • vector 和 list 相似,它支持高效的随机访问
  • map 是一种 “键值对” (Key-Value),在以后的文章中会详细介绍这种数据结构
  • set 是一种不能出现重复元素的集合

然而由于篇幅限制和本人水平有限
并不能详尽的说明每一种集合的每一种用法
本文以 list 和 vector 作简要介绍
更为详细的内容可以查阅相关 API[2] 文档


好吧读完上面的东西你可能想说

什么玩意儿

没事儿,文字描述总是很枯燥
我们更倾向于使用代码来向你介绍
我们使用 list 函数来创建一个 list (列表)

=> (list "hello" "list")
("hello" "list")

如你所见, list 函数的返回值就是一个 list。
列表里的每一个内容称之为元素,而元素被小括号包围,这就构成了一个列表。
此例中的这个 list 包含两个元素 --- 字符串"hello" 和字符串 "list"

你也许已经注意到了,list 的“样子”看起来很眼熟,
没错,它的书写方式和 Clojure 表达式一样,Clojure 语言本身就使用这种数据结构来表示语言本身。
使用自身的数据结构来表示自身
这种特征我们给它单独取一个名字,以表示高端,称之为 “同像性”[3]
又称 “代码即数据”

(如果你学习过编译相关的知识,你就能立刻发现这种做法的意义
Clojure 和所有的 Lisp 一样,直接使用语法树作为语言本身
这也是宏(Macro)功能的基础)

Clojure 还提供了一个语法糖[4] --- 单引号 ' ,它的功能是阻止求值。
它的非语法糖形式是 quote
quote 的返回值是其后的代码本身。
我们知道,Clojure 里的表达式都会被求值,如果我们使用阻止求值 quote 或者它的语法糖 ', Clojure 就会明白,你想要的不是这个表达式的值,而是需要这个表达式本身(也就是数据)。

=> (quote ("hello" "list"))
("hello" "list")

=> '("hello" "list")
("hello" "list")

如果不进行阻止求值,那么由于 “hello” 在括号的第一个位置,会被认为是一个函数
然而我们并没有这个函数,自然也就无法得到值,此时程序扔出一个错误

=> ("hello" "list")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
;意为:某个 String(即字符串)无法被作为 IFn(即函数)来使用

说了一大堆,我们再回到 list
因为同像性,我们可以使用阻止求值来得到一个 list。也可以使用 list 函数来得到一个 list。


你可能会发出这样的疑问

那么这又有什么屁用呢?

list 是一些 “元素” 的集合,这些元素可以是任意表达式,以先后顺序存放在 list 中。
仅仅是存储还不够,我们还可以对这个 list 进行各种操作。
这里介绍几个常用的操作:

  • firstsecond 函数分别用来取出集合中的第一个或第二个元素
=> (first '("hello" "list"))
hello

=> (second '("hello" "list"))
list
  • rest 函数用来返回除了 first 之后的元素,并以 list 的形式作为返回
=> (rest '("hello" "list"))
("list")
  • nth 函数用来取出任意指定位置的元素,表示这个位置的索引值放在第三个参数的位置
=> (nth '("hello" "list") 0) ;取出第 0 号元素
hello

注意,在大多数程序语言中,索引是以 0 开始的
也就是第一个元素的编号为 0,第二个元素的编号为 1 ...

list 可以按照你的直觉进行嵌套:
这样就使得你可以创造更为复杂的结构。
我们来看一下如何操作嵌套结构

=> (first '(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑"))
("屠龙宝刀" "点击就送")

=> (second (first '(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑")))
点击就送

我们来讲解一下上面第二句代码
首先我们使用 ' 来得到一个拥有 5 个元素的列表

'(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑"))

这里我们注意,使用 ' 会导致它之后的一个括号中的所有内容都被阻止求值
所以在内层的列表无需再次使用 '

虽然 list 函数也可以创建一个列表,但这里如果我们不用 ' ,而是直接使用 list 函数则会出现错误

=> (list ("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn

为啥呢?因为 list 函数只是把它收到的参数的来构建出一个列表。所以执行到需要得到 ("屠龙宝刀" "点击就送") 的值的时候,因为"屠龙宝刀" 在括号的第一个位置,所以把它当作函数进行处理了,就发生了上面的错误。

回头再来看这个列表,五个元素分别为

0号 ("屠龙宝刀" "点击就送")列表也可以作为一个元素(实际上任意表达式都可以作为元素)
 1号 "激光剑"
 2号 "无尽之刃"
 3号 "传送枪"
 4号 "物理学圣剑"

在它的外面我们又套了一层函数 first

(first '(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑"))

我们知道,first 函数的返回值,等于它的参数的第一个位置所存放的元素,
在这里即为 ("屠龙宝刀" "点击就送")

也就是说,
(first '(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑"))
等价于
("屠龙宝刀" "点击就送")

把这个值作为外层的 second 函数的参数,进行简单替换

替换前:

(second (first '(("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑")))

替换后:

(second '("屠龙宝刀" "点击就送"))

再执行 second 函数,取第二个位置,结果为 "点击就送"
(在某些环境下,字符串显示出来可能会带有双引号)


下面我们来看一下 vector
我们可以使用 vector 函数来创建一个 vector

=> (vector "hello" "vec")
["hello" "vec"]

注意到 vector 看起和 list 有所不同,它使用中括号(方括号)而不是小括号来包围元素。
我们也可以直接使用中括号(方括号)来得到一个 vector:

=> ["hello" "vec"]
["hello" "vec"]

(这里并不需要使用阻止求值,中括号是一种用来创建 vector 的特殊形式,Clojure 能正确的处理它,并不会和执行函数的小括号造成混淆。)

还记得本文第一句来自于第一位图灵奖得主 Alan J. Perlis 的格言么?
我们刚学到的可以用于 list 的函数也可以用于 vector

=> (first ["hello" "vec"])
hello

=> (second ["hello" "vec"])
vec

=> (rest ["hello" "vec"])
("vec") ;虽然这里我们使用 rest 操作的是 vector,但 rest 仍然返回 list 形式

=> (nth ["hello" "vec"] 1)
vec

同样可以嵌套

=> (second (first [["屠龙宝刀" "点击就送"] "激光剑" "无尽之刃" "传送枪" "物理学圣剑"]))
点击就送

我们甚至可以在 vector 里嵌套 list,或者在 list 里面嵌套 vector

=> (second (first ['("屠龙宝刀" "点击就送") "激光剑" "无尽之刃" "传送枪" "物理学圣剑"]))
点击就送

=> (second (first '(["屠龙宝刀" "点击就送"] "激光剑" "无尽之刃" "传送枪" "物理学圣剑")))
点击就送

这多亏了不同的数据结构使用了同一套抽象
我们得以用统一的形式(同一函数)来操作不同的数据结构,让你好似在操作同一种数据结构


list 和 vector 有很多共同点,就如同已经向你展示的一样
但 vector 拥有一些其它特性

  • 使用 subvec 函数来取出指定起止位置的内容
    后两个参数分别表示起止的索引位置(不包括结束位置元素)
=> (subvec [["屠龙宝刀" "点击就送"] "激光剑" "无尽之刃" "传送枪" "物理学圣剑"] 1 3)
["激光剑" "无尽之刃"]

这个例子给出的起止位置为 1 3,不包括结束位置的元素,也就是从 1 开始取到 2
结果组成一个 vector 作为返回值

  • 使用 assoc 函数来 “改变” 指定位置的内容
    这个函数的后两个参数分别是要 “改变” 的元素的位置和 “改变” 后的值。
    返回值是 “改变” 后的结果。
=> (assoc [["屠龙宝刀" "点击就送"] "激光剑" "无尽之刃" "传送枪" "物理学圣剑"] 1 "lightsaber")
[["屠龙宝刀" "点击就送"]
 "lightsaber"
 "无尽之刃"
 "传送枪"
 "物理学圣剑"]

注意我们使用了带引号的 “改变”,这表示 assoc 函数实际上并没有对原来的 vector 做出任何改变。
这个特性会在之后的文章中进行说明。

  • nth 语意可以直接被使用
=> (nth ["hello" "vec"] 1)
vec

=> (["hello" "vec"] 1) ;比上面使用 nth 更方便
vec

其实 ["hello" "vec"] 本身就是个函数,所以可以把它放在函数的位置
这个函数的功能和 nth 一样,所以我们可以方便的从 vector 里面取数据


你可以把你学到 first second rest 用于 map 和 set
但你不能把 nth 用于 map

原因是 map 没有实现 Indexed,而 nth 只能作用于实现了 Indexed 的集合。
first second rest 可以作用于实现了 Sequence 的集合,所有的 Clojure 集合都实现了 Sequence。

这些关于集合的抽象实现内容并不需要在现在就掌握,感兴趣可以自行查询。

map 会在之后的章节中做详细介绍,它是 Clojure 中非常实用的一种数据结构

如果你在阅读本文时感到吃力,你可以先把文中的代码在你的机器上运行一下
观察运行结果,然后试着更改参数,看看返回结果是否满足你的猜想
待你基本了解工作效果之后,再次阅读本文
这种学习方式会帮助你更好地理解本文(或者其它程序设计类教程)

除了本文介绍的方法之外,操作集合的函数还有很多
你可以访问 http://clojuredocs.org/ 查阅 API[2] 的小例子
也可以访问官方网站 http://clojure.github.io/clojure/ 来直接查阅官方 API 说明
英文苦手们可以访问中文翻译项目 http://clojure-api-cn.readthedocs.io/en/latest/


  1. 数据结构:简单来说,就是如何“摆放”值,和如何对值进行操作的一种抽象

  2. Application Programming Interface,即应用编程接口。是一种描述函数如何工作的描述文档

  3. 同像性并不是 Clojure 的专利,其它很多语言也具有同像性,包括所有 Lisp 方言、机器语言(汇编语言)、Prolog 等

  4. 语法糖:便于程序员书写的一种简化语法的 “甜甜的” 东西

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容