Clojure 学习入门(18)—— 数据类型

如何表示和处理数据

Clojure是一种动态类型语言,这意味着你在程序中永远不需要明确定义符号、函数、或者参数的数据类型。但是,所有的值仍然有一个类型。字符串时是字符串,数字是数字,列表是列表,等等。如果你尝试执行一个类型不支持的操作,将会在运行时产生错误。写代码时避免这种事情,是程序员的责任。对于有动态语言背景的人来说是很自然的事情,而那些只使用静态语言的人需要一些转变。

Clojure的类型既简单又复杂。Clojure的本身只有少量不同的类型,而且Clojure不是面向对象语言,它本身并不支创建新的用户自定义类型。一般来说,这让事情非常简单。但是,Clojure运行在Java虚拟机上,在内部每个Clojure的类型也表现为一个Java的类或接口。此外,如果你关联了一个Java库,你可能要注意Java类和类型。幸运的是,通常你只需要在Clojure中关联Java代码时关注它。

表4-1. Clojure的内置类型
类型 描述 例子 内部Java类/接口
Number 数字本身 16 java.lang.Number
String 用双引号包围 "Hello!" java.lang.String
Boolean true 或者 false true ava.lang.Boolean
Character 前缀一个反斜线 \a java.lang.Character
Keyword 前缀一个冒号 :key clojure.lang.Keyword
List 括号 '( 1 2 3 )
Vector 方括号 [ 1 2 3 ]
Map 花括号 {:key val :key val} java.util.Map
Set 花括号前缀井号 #{ 1 2 3 } java.util.Set

Nil

保留符号nil在Clojure程序中有特殊的意义:它的意思是“空”或“空值”。 当nil用于布尔表达式计算和空判断时永远返回false,但是它不等于它自己。 它可用于任何数据类型,包括原语。但是,传递nil给大多数函数或操作将导致一个错误,因为它不是一个真正的值。 如果一个值有可能是nil,你就需要考虑代码中的这种特殊情况,以避免这个操作会产生一个java.lang.NullPointerException错误。 nil和Java中的Null是相同的。

基本类型

Clojure提供了一些基本类型来表示基本程序语言的类型结构,比如数字,字符串和布尔值。

Numbers

Clojure 对数值和数值计算有非常好的支持,数字常量能够用多种方式表示:

  • 标准计数法的整形和浮点小数直接作为数字类型。例如,42或者3.14159。
  • Clojure还支持直接使用/符号输入比率常量。例如,5 / 8或3 / 4。用比率输入的常量将会自动简化。如果你输入4 / 2,将被简单的存储为2。
  • 你能够以基数+r+值的形式输入任何整型常量。例如,2r10是二进制的2,16rFF是十六进制的255,你甚至可以输入像36r0Z这样的东西,这是35的36进制表示。语言支持2到36之间的所有基数。
  • Clojure还支持ava传统的十六进制和八进制表示法。数字前缀0x是十六进制表示:例如,0xFF是255。数字前缀0的都被为是八进制。
  • 任何计算机上,对于十进制数都有两种表示法:浮点数和一个精确的十进制值。Clojure和Java一样,默认使用浮点数表示法,也不支持精确计算,内部使用Java的java.math.BigDecimal类。要指定一个常量内部使用恰当的精确形式,需要在数字后边添加一个M。例如,1.25M。与浮动点不同的是,这些数字将被四舍五入操作。这使得他们大多数的时候更加适用于货币。

警告因为Clojure使用Java的整型字面量约定,前缀0的数字被认为是八进制数,如果你强制输入类似09这样的数字会返回一个错误,因为它不是有效的八进制数。


在涉及到不同类型的数字的运算时,Clojure会自动将结果转换到最精确的类型。例如,当整数和浮点数相乘时,结果将是浮点数。除法运算总是返回一个比率,除非其中一项是个十进制数,结果会被转换成浮点数。

数字没有最大值的限制。Clojure会自动转换为最合适的内部表示形式来表示越来越大的数字,处理任何数字都没有问题。然而,在高性能应用中要注意,当操作的数据大小超过Java Long类型时,也就是数字超过9,223,372,036,854,775,807时,你可能会感到运行缓慢。这种需要不同内部表示的形式对于高速数学运算不是高效的,即使它是足以满足大多数任务。尽管它足以满足绝大多数任务。

常见数值函数

这些函数提供了对数字的数学运算。
注释 为保持简单,Clojure API中的计算函数与其他语言的常见运算是一致的。但不用担心:当表达式被解析和编译时,它们会被替换为优化的Java字节码,尽可能使用原始运算。为保持简单作为函数的数学运算没有损失任何速度。

加法 (+)

加法函数(+)接受任意数值类型的参数,返回它们的和。
(+ 2 2)
-> 4
(+ 1 2 3)
-> 6

减法 (-)

减法函数(-)接受任意数值类型的参数。如果只有一个参数,则返回它相反的数。当有多个参数时,返回第一个参数减去后面所有参数的结果。
(- 5)
-> -5
(- 5 1)
-> 4
(- 5 2 1)
-> 2

乘法 (*)

乘法函数 ( *) 接受任意数值类型的参数并返回它们的乘积。
(* 5 5)
-> 25
(* 5 5 2)
-> 50

除法 (/)

除法函数 (/) 接受任意数值类型的参数。第一个参数是分子,其他任意参数是分母。如果没有分母,则函数返回 1 / 分子,否则返回分子除以分母。
(/ 10)
-> 1/10
(/ 1.0 10)
-> 0.1
(/ 10 2)
-> 5
(/ 10 2 2)
-> 5/2

增量

增量函数 (inc) 接受一个数值类型参数并返回它的值加1。
(inc 5)
-> 6

减量

减量函数 (dec) 接受一个数值类型参数并返回它的值减1。
(dec 5)
-> 4

商函数 (quot) 接受两个数值类型参数并返回第一个参数除以第二个参数的整数商。
(quot 5 2)
-> 2

余数/模数函数 (rem) 接受两个数值类型参数并返回第一个参数除以第二个参数的余数。
(rem 5 2)
-> 1

最小数

最小数函数 (min) 接受任意数值类型的参数并返回最小的。
(min 5 10 2)
-> 2

最大数

最大数函数 (max) 接受任意数值类型的参数并返回最大的。

(max 5 10 2)
-> 10

等于 (==)

等于函数 (==) 接受任意数值类型的参数,如果它们相等返回ture,否则返回flase。
(== 5 5.0)
-> true

小于 (<)

小于函数 (<) 接受任意数值类型的参数,如果它们按升序排列返回true,否则返回false。
(< 5 10)
-> true
(< 5 10 9)
-> false

小于等于 (<=)

小于等于函数 (<=) 接受任意数值类型的参数,如果它们按升序排列或顺序相等返回true,否则返回false。
(<= 5 5 10)
-> true

大于 (>)

大于函数 (>) 接受任意数值类型的参数,如果它们按降序排列返回true,否则返回false。
(> 10 5)
-> true

大于等于 (>=)

大于等于函数 (>=) 接受任意数值类型的参数,如果它们按降序排列或顺序相等返回true,否则返回false。
(>= 10 5 5)
-> true

0检查

0检查函数 (zero?) 接受一个数值类型参数,如果是0返回true,否则返回false。
(zero? 0.0)
-> true

正数检查

正数检查函数 (pos?) 接受一个数值类型参数,如果是大于0返回true,否则返回false。
(pos? 5)
-> true

负数检查

负数检查函数 (neg?) 接受一个数值类型参数,如果是小于0返回true,否则返回false。
(neg? -5)
-> true

数值检查

数值检查函数 (number?) 接受一个参数,如果是数值返回true,否则返回false。
(number? 5)
-> true
(number? "hello")
-> false

字符串

Clojure字符串和Java字符串相同,都是java.lang.String类的实例。它们作为文本输入,用双引号括起来。如果需要在字符串中写双引号字符,可以使用反斜杠字符进行转义,\.例如,下面这个有效的字符串:
"Most programmers write a \"Hello World\" program when they learn a new language"
在字符串中输入反斜杠字符,只需输入两个反斜杠。

常用字符串函数

Clojure仅提供了少量方便的字符串函数。对于更高级的字符串操作,你既可以使用Java字符串API(参见本章与Java的交互操作),也可以使用clojure.contrib用户库的str-utils命名空间定义的各种各样的字符串工具函数。

连接

字符串连接函数 (str) 接受任意数量的参数。如果参数不是字符串则将其转换为字符串,返回连接创建的新字符串。如果没有参数或为nil,则返回空字符串,""。
(str "I have " 5 " books.")
-> "I have 5 books."

子串

子字符串函数 (subs) 接受两个或三个参数, 第一个是字符串,第二个是一个整数偏移量,第三个(可选)是另一个整数偏移量。函数返回从第一个偏移量(含),到第二个(不含)偏移量或者结尾(如果没有第二个偏移量)截取的子字符串。
(subs "Hello World" 6)
-> "World"
(subs "Hello World" 0 5)
-> "Hello"

字符串检查

字符串检查函数 (string?) 接受一个参数,如果是字符串返回true,否则返回false。
(string? "test")
-> true
(string? 5)
-> false

打印与换行打印

字符串打印函数 (print & println) 接受任意数量参数,打印到标准系统输出(如果不是字符串则转换成字符串)。println 在尾部追加一个换行符。两个函数都返回nil。

正则表达式函数

Clojure提供了一些函数用于处理正则表达式,包装了Java正则表达式实现。

re-pattern

函数 (re-pattern) 接受一个字符串参数,返回一个正则表达式样式(java.util.regex.Pattern类的实例)。这个样式能用于正则表达式匹配。
(re-pattern " [a-zA-Z]*")
-> #"[a-zA-Z]*"
也可以使用读取宏来直接用文本的方式输入正则表达式:在字符串前使用#符号。结果同样是样式,和用re-pattern函数生成的一样,例如,下面的表示方式和前面的例子是相同的:
#" [a-zA-Z]* "
-> #"[a-zA-Z]*"

re-matches

re-matches接受两个参数:一个正则表达式样式和一个字符串。返回任何和正则表达式样式匹配的字符串,如果没有匹配则返回nil。例如下面的代码:
(re-matches #"[a-zA-Z]* " "test")
-> "test"
(re-matches #"[a-zA-Z]* " "test123")
-> nil

re-matcher

re-matcher接受两个参数:一个正则表达式样式和一个字符串。返回一个有状态的"matcher"对象,提供给其它正则函数而不是直接提供样式。Matchers是java.util.regex.Matcher.类的实例。
(def my-matcher (re-matcher #" [a-zA-Z]* " "test")
-> #'user/my-matcher

re-find

re-find接受一个样式与一个字符串或者一个matcher。每次调用,返回matcher中下一个符合正则匹配的结果(如果还有)。
(re-find my-matcher)
-> "test"
(re-find my-matcher)
-> ""
(re-find my-matcher)
-> nil

re-groups

接受一个matcher,返回从接近的发现与匹配的集合。如果没有嵌套集合,则返回一个完全匹配的字符串。如果有嵌套集合,则返回vector集合,第一个元素是完全匹配的(非嵌套)。

re-seq

re-seq接受一个样式与一个字符串。它返回一个使用永久匹配(matcher)的lazy sequence(懒序列 见第5章)(这个sequence在一个连续的样式匹配的字符串中)。
(re-seq #" [a-z] " "test")
-> ("t" "e” "s" "t")

布尔

布尔值在Clojure中非常简单。使用文本值的保留符号true和false,并使用java.lang.Boolean类作为底层。当计算其它数据类型的布尔表达式的时候,所有数据类型(包括空字符串、空集合、和数值0)均按true计算。除了实际布尔值false,计算结果为false的都是空值 nil。

常用布尔函数

Clojure提供了一些方便的布尔函数。

not

not 函数(not) 接受一个参数。如果逻辑计算结果是false则返回true,如果逻辑计算结果是true则返回false。
(not (== 5 5))
-> false

and

and宏接受任意数量参数,如果每个逻辑计算结果都为true则返回true,反之为false。为保证效率,如果第一个参数的结果是false,则直接返回false而不再计算其他参数。
(and (== 5 5) (< 1 2))
-> true

or

or宏接受任意数量参数,如果参数逻辑结果有一个或多个为true则返回true,反之为false。 为保证效率,只要一个参数的逻辑结果为true,就返回true而不再计算其他参数。
(or (== 5 5) (== 5 4))
-> true

字符

字符用来表示单个Unicode字符。输入一个字符文本,前缀一个反斜杠,例如,\i是字符"i"。任何Unicode字符都能用一个反斜杠,加上一个'u'和Unicode字符的四位十六进制代码输入。

例如,\u00A3是£符号。Clojure也很容易输入空白字符文本,支持下列特殊值:\newline(新行),\space(空格)和\tab(制表符)。

Char

字符强制转换函数 (char) 接受一个整型参数并返回对应的ASCII/Unicode字符。
(char 97)
-> \a

Keywords

关键字是Clojure中唯一特殊的的原始数据类型。主要目的是提供非常高效的存储和相等判断。因此,关键字理想的用途是作为map数据结构的key或者其它简单“标记”功能。作为文本,关键字在开头带冒号,例如:keyword。在冒号后面,它们遵循和符号相同的命名规则(见第2章)。
关键字能够作为命名空间(可选的)。如关键字:user/foo,是指在user命名空间中叫做foo的关键字.命名空间关键字能够通过输入完全限定名或前缀两个冒号在当前命名空间中查询来引用(例如,如果当前命名空间都是user,::foo 和:user/foo 是相同的)。

keyword

关键字函数 (keyword) 接受一个字符串参数,并返回一个同名的关键字。如果有两个参数,返回一个带命名空间的关键字。

(keyword "hello")
-> :hello用法
(keyword "foo" "bar")
-> :foo/bar

keyword?

关键字检查函数接受一个参数,如果是关键字返回true,否则返回false。

(keyword? :hello)
-> true
	namespace
…....

组合类型

Clojure的组合数据类型是用来高效地满足操纵各种聚合数据结构的需要。这些数据类型经过优化之后效率更高,并且与Clojure的其它部分以及Java更加兼容,并且坚持了Clojure的原则:不变性。如果这些数据类型中的任何一种都不足以表示某种数据结构,那么我们可以通过任何方式来组合它们。
这些数据类型都具有如下性质:
都不可变。一旦被创建,它们就不可改变,因此对于任何时间的任何线程来讲,访问它们都是安全的。那些被认为是“改变了“它们的操作实际上是返回了一个全新的依旧不可变的对象。
都是持久的。这些数据类型会快速地与它们的之前版本共享数据结构来持久化内存和运行时间。因此,它们都十分的快速和高效,在某种程度上来讲,跟那些在其它语言中的可变的类似概念相比,更加高效。
适当地支持判断是否相等的语义。这意味着若两个对象的数据类型相同且包含相同引用,它们总是被认为是相同的,而不管其实例化和实现的细节。因此,两个组合类型的数据,即使创建于不同的时间或不同的地点,也依然可以用来比较。
在Clojure中使用起来十分简单。每种组合数据类型都有一个方便的字面表示和许多相关函数,确保使用这些数据类型顺利无碍。
支持与Java的互操作。这些数据类型都很好地支持了标准java.util.Collection框架的只读部分。在很多情况下,这表示它们可以不用更改地传递给那些需要组合数据类型的Java对象和方法。Lists实现了java.util.List,Maps实现了java.util.Map,Sets实现了java.util.Set。注意,因为它们都是不可变的,所以如果你使用了可能改变它们的方法,就会抛出一个UnsupportedOperationException异常。这与文档中规定的java.util.Connections接口标准一致,因为组合数据类型不支持“破坏性的“改变。
基于函数编程的范式,这些数据类型都支持通过简单而强大的操作来操作序列。这些功能在第五章有详细讨论。

列表

对Clojure来说列表十分重要,因为实际上Clojure程序本身就是由很多嵌套着的组成的。在最基本的层面上来讲,一个列表就是一些元素的有序集合。
列表可以通过使用括号来直接输入,这也是为什么Clojure代码本身就使用了如此多的列表。例如,正常地调用一个函数:
(println "Hello World!")

它是一串可执行的代码,同时也是一个列表。首先,Clojure读取程序将它作为一个列表来解析,然后将其第一个元素(在这里是println)作为函数来对它求值,然后将剩余的部分

("Hello World!")作为参数传递给它。
如果只是作为数据结构而不是可执行代码来使用列表,只需要给列表加一个单引号作为前缀即可。这告诉Clojure将其作为数据结构来对待,而不是将其当作Clojure形式对其求值。例如,定义一个由1到5组成的列表,并将其绑定到一个符号,你可以这样做:
(def nums '(1 2 3 4 5))

注意:这里的单引号实际上是另一种形式,叫做quote。'(1 2 3)和(quoto (1 2 3))只是表示相同事物的不同方法而已。quote(或者单引号)可以在任何地方使用,来阻止Clojure立即对一个表达式求值。实际上,它的作用远不止于声明一个列表,当涉及到元编程的时候,单引号十分必须。请阅读12章里在宏里使用quote来实现复杂的元编程的详细讨论。


列表是以单向链接列表的形式来实现的,在这一点上有利有弊。读取列表的第一个元素或者在列表头添加一个元素的操作都可以在常量时间内完成,然而访问列表的第N个元素却需要N次操作。因为这个原因,在很多情况下,向量是个更好地选择。不过列表在很多情况下依然十分有用,特别是在即使构建Clojure代码的时候。

listlist函数接收任意数量的参数并将它们的值组成列表。

(list 1 2 3)
--> (1 2 3)

peekpeek函数操纵一个单一的列表作为参数并返回列表中的第一个值。

(peek '(1 2 3))
--> 1

poppop函数操纵一个单一的列表作为参数并且返回一个去掉了首个元素的新列表。

(pop '(1 2 3))
--> (2 3)

list?如果其参数是一个列表,那么列表测试函数list?返回true,否则返回false。

(list? '(1 2 3))
--> true

向量

向量跟列表很相似,它们都存储一串有序的元素。但是,它们有一个很重要的地方有所不同:向量支持高效地、近乎常量时间地根据元素的索引来访问。从这一点来看,相比于列表,向量更像是数组。总的来说,对于很多应用来讲向量更好,因为跟列表相比向量毫无劣势而且更快。
向量在Clojure程序中的字面表示是使用方括号。例如,一个由1到5组成的向量可以通过如下代码定义并绑定到一个符号上:
(def nums [1 2 3 4 5])
向量的它们的索引的函数。这不仅仅是一个数学上的描述——它们都是实现了的函数,并且可以通过函数调用来取得元素的值。通过索引来取得值的最简单的方法是:像函数一样调用这个向量,然后将你想要的索引传递给它。索引从0开始,所以,为了取得之前定义好的一个向量的第一个元素,你可以这样做:
user=> (nums 0)
1

尝试访问超出向量长度的索引会引发一个错误,具体来说是java.lang.IndexOutOfBounds异常。

vector构建向量函数vector接收任意数量的参数并将它们的值组成一个向量。

(vector 1 2 3)
--> [1 2 3]

vec向量转换函数vec接收一个单独的参数,可能是任何Clojure或Java的组合数据类型,然后将其元素的值作为参数组成一个新的向量。

(vec '(1 2 3))
--> [1 2 3]

getget函数接收两个参数来操作向量。第一个参数是一个向量,第二个参数是一个整数索引。它返回给定索引处的值,若在索引处没有值,则返回nil。

(get ["first" "second" "third"] 1)
--> "second"

peekpeek函数接收一个单独的向量作为参数,并且返回向量的最后一个值。考虑到列表和向量的不同实现方式,这跟列表的peek函数有所不同:向量总是访问最方便的那个元素。

(peek [1 2 3])
--> 3

vector?向量测试函数vector接收一个单独的参数,若参数是一个向量则返回true,否则返回nil。

(vector? [1 2 3])
--> true

conj连接函数conj接收一个组合数据类型(例如向量)作为其第一个参数和任意数量的其它参数。它返回一个新的向量,这个向量由将所有的其它参数连接到原来那个向量尾部组成。conj函数也对映射和集合适用。

(conj [1 2 3] 4 5)
--> [1 2 3 4 5]

assoc向量组合函数assoc接收三个参数:第一个是向量,第二个是整数索引,第三个是一个值。它返回一个新的向量,这个向量是原来那个向量在给定的索引处插入那个值的结果。如果索引超过了向量的长度,那么会引发一个错误。 (assoc 2 3 1 "new value") --> "new value" 2 3

poppop函数接收一个单独的向量作为参数,并且返回一个去掉尾部元素的新向量。考虑到列表和向量的不同实现方式,这跟列表的peek函数有所不同:向量总是访问最方便的那个元素。

(pop [1 2 3])
--> [2 3]

subvec子向量函数subvec接收两个或三个参数。第一个是一个向量,第二个和第三个(如果有的话)是索引。它返回一个新向量,这个向量由原来那个向量的介于两个索引之间或者第一个索引到向量末尾(如果没有第二个索引)的部分组成。

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

Maps

映射可能是Clojure里最有用最强大的内建组合数据类型了。实际上,映射十分简单。它存储一个键-值对的集合。键和值都可以是任何数据类型的对象,无论是基本数据类型还是其它映射。然而,使用关键字来作为映射的键非常合适,因此它们经常在应用映射的场合被使用。
映射的字面表示是使用花括号包围着的偶数个元素。这些元素都被看作键/值对。例如这个映射:
(def my-map {:a 1 :b 2 :c 3})
这个映射定义定义了一个有三个键的映射,关键字:a,:b和:c。键:a绑定到1,:b绑定到2,:c绑定到3。由于逗号在Clojure中和空格的作用是一样的,它经常被用来清晰地表示键/值对,而丝毫不改变映射定义的实际意义。下面这行代码跟之前的那行完全相同:
(def my-map {:a 1, :b 2, :c 3})
虽然关键字作为映射的键十分合适,但是并没有规则说你必须要使用它们:任何值,甚至是另一个组合数据类型,都可以作为键。关键字、字符串和数字都经常被用作映射的键。
与向量类似,映射是它们的键的函数(不过如果给定的键不存在,它们不会抛出异常)。要得到一个特定键对应的值,只要使用该映射最为函数,并将键作为参数传递给它。例如,为了得到上面的例子里:b对应的值,只需要这样做:
user=> (my-map :b)
2
普通的映射可能有三种不同的实现方式:数组映射、哈希映射和有序映射。它们分别使用数组、哈希表和二叉树来作为底层实现。数组映射最适用于较小的映射,而对哈希映射和有序映射的比较则要基于特定应用场合的情况。
默认地,根据字面定义的映射如果很小则被实例化为数组映射,若很大则为哈希映射。若要显式地创建特定类型的映射,可以使用 hash-map或者 sorted-map函数:
user=> (hash-map :a 1, :b 2, :c 3)
{:a 1, :b 2, :c 3}


user=> (sorted-map :a 1, :b 2, :c 3)
{:a 1, :b 2, :c 3}
注意,哈希映射并不储存键的顺序,而有序映射则会根据键来对值进行排序然后存储。默认地,sorted-map非常自然地对键进行比较:根据数字或者字母表里可用的那一种。

Struct Maps

使用映射时,很多时候有这种情况:我们需要产生一组有相同键组合的映射。因为一个普通的映射对它键和值都会分配内存,所以在产生大量的类似映射的时候这会导致内存的浪费。
不过,创建大量映射很多时候十分有用,所以Clojure提供了结构映射。结构映射允许你首先定一个键组成的结构,然后用它来实例化多个映射,并通过共享键和查找的信息来节省内存。它们在语义上跟普通映射相同:唯一不同的是实现方式。
要定义一个结构,使用 defstruct:它接收一个名字和一些键作为参数。例如如下代码:
(defstruct person :first-name :last-name)

这定义一个名为person的结构,键为:first-name:last-name。使用struct-map函数来创建person的实例:

(def person1 (struct-map person :first-name "Luke" :last-name "VanderHart"))
(def person2 (struct-map person :first-name "John" :last-name "Smith"))
现在,person1和person2是两个不同的映射并且十分节省地共享键的信息。但是他们依然是映射,因此从各方面来说,你都可以使用相同的方法来取得一个值甚至是添加新的键。当然,新添加的键不会像在结构里定义的键一样有节省内存的优势。跟普通映射相比,结构映射的唯一限制是,你不能删除一个结构映射里的某个在结构定义里定义了的键。这样错会引发一个错误。
结构映射同时允许你创建十分高效的函数来访问键的值。普通映射的查找速度绝不慢,但使用结构访问函数,你将可以大大缩短普通键查找过程所花的时间,以适用于那些极端性能敏感场合的应用。
要创建一个结构映射的高性能访问函数,使用 accessor函数。它接收一个结构定义和一个键作为参数,并返回一个一等888函数作为返回值。这个函数接收一个结构映射作为参数,并返回一个值。

accessor

(def get-first-name (accessor person :first-name))

你可以使用新定义的get-first-name函数十分快速地取得一个函数映射里的:first-name键对应的值。下面的两个表达式是等价的,但是使用访问函数的版本更快。

(get-first-name person1)
(person1 :first-name)
总的来讲,除非是考虑到性能的因素,你无须担心要去使用结构映射。对很多应用来讲,普通映射就足够快了,而且结构映射只是稍微增加了复杂度,而除了些许的性能提升外并没有特别的好处。你应该知道它们,因为它们使得一些程序的性能更好,但实际上最好是你现实用普通映射,之后优化的时候再使用结构映射来重构你的程序。

Maps as Objects

很明显,在很多场景下映射都十分有用。编程时,连接键和值是一个很常见的操作。然而,映射的可用性远远不止于我们所认为它只是一个数据结构的那样。
一个很重要的例子是,结构可以做到面向对象编程中的对象90%能做的事。那么对象中命名的属性和映射里的键/值对到底有什么不同之处呢?像Javascript这种语言(对象是用映射实现的)表示,没有什么不同。
好的Clojure程序大量使用这种映射即是对象的观点。虽然Clojure在总体上不接受面向对象的理念,对面向对象设计的数十年的研究确实发现了一些关于数据包装和组织的好的规则。这样使用Clojure的映射的话,那么从面向对象的数据组织里获得某些技巧和教训并且规避它的缺点就变得可能了。在一个Clojure程序的上下文里,使用映射十分不错,因为可以通过普通的方式来操作它们,而不必为不同的类的对象创建操作的方法。

assoc

映射结合函数assoc接收一个映射和一些键/值对作为参数。它返回一个新的映射,此映射含有参数里提供的键,或者替换原映射里任何已有的键。

(assoc {:a 1 :b 2} :c 3)
-> {:c 3, :a 1, :b 2}

(assoc {:a 1 :b 2} :c 3 :d 4)
-> {:d 4, :c 3, :a 1, :b 2}

dissoc

映射分离函数dissoc接收一个映射和一些键作为参数。它返回一个新的映射,此映射去掉了参数了提供的这些键。

(dissoc {:a 1 :b 2 :c 3} :c)
-> {:a 1, :b 2}

(dissoc {:a 1 :b 2 :c 3 :d 4} :a :c)
-> {:b 2, :d 4}

conj

连接函数conj对映射的作用跟对向量的作用一样,不过连接的不是一个单独的元素,而是一个键/值对。

(conj {:a 1 :b 2 :c 3} {:d 4})
-> {:a 1, :b 2, :c 3, :d 4}
连接一个键/值对组成的向量也可以,例如如下代码所示:
(conj {:a 1 :b 2 :c 3} [:d 4])
-> {:d 4, :a 1, :b 2, :c 3}

merge

映射归并函数merge接收任意数量的参数,其中每个参数都是一个映射。它返回一个新的映射,该映射有参数里所有映射的键/值对组成。若某一个键出现在了多个映射里,最终其值会是最后包含此键的映射里对应的值。

(merge {:a 1 :b 2} {:c 3 :d 4})
-> {:d 4, :c 3, :a 1, :b 2}

merge-with

merge-with函数接收一个一等函数作为其第一个参数,然后是任意数量的映射作为其它参数。它返回一个新的映射,该映射由参数里的所有映射的键和值所组成。若一个键在多个映射里出现,那么最后的值是参数里给定的函数作用于所有这些冲突键的值的返回值。

(merge-with + {:a 1 :b 2} {:b 2 :c 4})
-> {:c 4, :a 1, :b 4}

get

get函数接收一个映射作为其第一个参数,一个键作为其第二个参数。第三个参数是可选的,是一个值,若没有找到参数里指定的键,则返回该值。它返回映射里指定键对应的值,若未找到并且第三个参数没有被指定,则返回nil。

(get {:a 1 :b 2 :c 3} :a)
-> 1

(get {:a 1 :b 2 :c 3} :d 0)
-> 0

contains?

contains?函数接收一个映射和一个键作为参数。若映射里存在该键,则返回true,否则返回false。除了映射,它也适用于向量和集合。

(contains? {:a 1 :b 2 :c 3} :a)
-> true

map?

映射测试函数map?接收一个单独的参数,若它是映射则返回true,否则返回false。

(map? {:a 1 :b 2 :c 3})
-> true

keys

keys函数接收一个单独的参数,一个向量。它返回一个由该向量里所有键组成的列表。

(keys {:a 1 :b 2 :c 3})
-> (:a :b :c)

vals

vals函数接收一个单独的参数,一个向量。它返回一个由该向量里所有值组成的列表。

(vals {:a 1 :b 2 :c 3})
-> (1 2 3)

Sets

Clojure里的集合的概念跟数学紧密相关:它们是不同的数据的集合,而且支持验证是否是集合的成员及其一般的集合运算,例如并、交和差。
集合的字面语法是一个井号后面跟着包围在花括号里的集合成员。例如如下的代码:
(def languages #{:java :lisp :c++})
跟映射一样,它们支持任何类型的对象作为其成员。例如一个使用字符串的类似集合:
(def languages-names #{"Java" "Lisp" "C++"})
集合的实现方式跟映射十分类似。它们都可以使用哈希表或二叉树来实现,使用 hash-set或者 sorted-set函数:
(def set1 (hash-set :a :b :c))
(def set2 (sorted-set :a :b :c))
跟映射相似,集合是它们的成员的函数。将一个集合调用为函数,并将一个值传递给它,若该值是集合的成员则会返回这个值,否则返回nil。
(set1 :a) ;return :a
(set1 :z) ;return nil

一般集合函数

注意,集合的关系函数并不在默认的clojure.core命名空间里,而是位于clojure.set命名空间。你要么显示地引用,要么使用ns形式的:use子句将其包含到你的命名空间里。请查阅第二章。

clojure.set/union

集合的并函数union接收任意数量的参数,每个参数都是一个集合。它返回一个新的集合,该集合由参数给定的集合的成员的并集组成。

(clojure.set/union #{:a :b} #{:c :d})
-> #{:a, :b, :c, :d}

clojure.set/intersection

集合的交函数intersection接收任意数量的参数,每个参数都是一个集合。它返回一个新的集合,该集合由参数给定的集合的成员的交集组成。

(clojure.set/intersection #{:a :b :c :d} #{:c :d :f :g})
-> #{:c, :d}

clojure.set/difference

集合的交函数intersection接收任意数量的参数,每个参数都是一个集合。它返回一个新的集合,该集合由参数中第一个集合里的元素里不在之后的其它集合里的元素组成。

(clojure.set/difference #{:a :b :c :d} #{:c :d})
-> #{:a, :b}

总结

Clojure提供了一组完整的强大的数据类型,使用它们可以满足任何程序的需求。它的基本类型提供了构建程序的基本组成部分,包括丰富简便的数字和字符串支持。 然而,Clojure的类型系统的真正威力在于它的集合数据类型库。组合数据类型不仅使用方面,更加补充了Clojure对于数据和不可变性的哲学。它严格遵守的原则有不可变性,意味着数据不可改变,持久性,意味着它们最大限度地高效共享其结构。依靠Clojure的内建数据结构并且熟悉可以操作它们的方法会十分有助于你构建高效、清晰和符合惯例的程序。


参考推荐:

Clojure 的数据

Clojure 判断&基本类型


你可能感兴趣的:(Clojure 学习入门(18)—— 数据类型)