我们上一节介绍了(def)
和(fn)
两个特殊形式(special form)。这里还有几个需要你马上掌握的特殊形式,它们构成了语言的基础词汇表。Clojure中还有大量实用的形式和宏,用得越多,认识会越来越深刻。
Clojure中的函数非常多,托它们的福,你能想到的任务很多都可以用Clojure完成。不要因此而沮丧,你应该感到庆幸。你要干的活大部分都有人替你干了,不该高兴吗?
我们在这一节会讨论特殊形式的基本工作集,然后是Clojure的原生数据类型(相当于Java的集合)。之后会接着讨论Clojure代码的自然编写风格——以函数而不是变量为中心。JVM面向对象的性质在底层还会存在,但Clojure强调函数的那种力量在纯粹的面向对象语言中表现得不太明显。
10.2.1 特殊形式新手营
表10-1给出了一些最常用的Clojure特殊形式。你现在最好快速地把这张表过一遍,然后在10.3节遇到具体例子时再回来看看。
表10-1 Clojure一些基本的特殊形式
(def
符号> <值?>)把符号绑到值上(如果有的话)。如有必要创建与符号对应的var
(fn
名称? [参数*
] <表达式>*
)返回带有特定参数的函数值,并把它们应用到表达式上。通常跟(def)
相结合,变成形式(defn)
(if<test> <then> <else>?)
如果test
的计算结果为true
,计算then
并产出其结果。否则计算else
并产出其结果,当然,前提是else
存在(let
[绑定>*
] <表达式>*
)给局部名称分配别名值,并隐式定义一个作用域。使得在let
作用域内的所有表达式都能获得该别名(do
表达式>*
)按顺序计算表达式的值,并产出最后一个的结果(quote
形式>)照原样返回形式(不经计算)。它只能接受一个形式参数,其他的参数全都会被忽略(var
符号>)返回与符号对应的var
(返回一个Clojure JVM对象,不是值)这个特殊形式列表不算详尽,并且其中很多特殊形式都有多种用法。表10-1中只是它们的基本用例,而且都不全面。
现在你对一些特殊形式的基本语法有进一步的了解了,让我们转去看看Clojure的数据结构吧,也看看它们怎么操作数据。
10.2.2 列表、向量、映射和集
Clojure中有几个原生数据类型。用的最多的是列表(list),即单向链表。
列表通常都用括号围起来,因为形式一般也是用圆括号,所以这算是一个轻微的语法障碍。况且括号还用来调用函数。所以初学者经常会犯下面这种错误:
1:7 user=> (1 2 3)java.lang.ClassCastException: java.lang.Integer cannot be cast toclojure.lang.IFn (repl-1:7)
之所以会出错,是因为Clojure中的值非常灵活,它希望第一个参数是函数值(或绑定到函数值上的符号),把2和3当做这个函数的参数。可在上例中1不是函数值,所以Clojure无法编译。按我们的说法,这个s表达式是无效的。只有有效的s表达式才能作为Clojure形式。
解决办法是用(quote)
形式,它的缩写是/'
。所以我们可以用两种方式定义列表:
1:22 user=> /'(1 2 3)(1 2 3)1:23 user=> (quote (1 2 3))(1 2 3)
(quote)
以一种特殊的方式处理它的参数。具体来说就是它不会计算参数,所以第一个参数不是函数值也没问题。
Clojure的向量(vector)跟数组类似,实际上,基本上可以把Clojure列表等同于Java的LinkedList
,向量等同于ArrayList
。向量可以用方括号表示,所以下面这些定义都一样:
1:4 user=> (vector 1 2 3)[1 2 3]1:5 user=> (vec /'(1 2 3))[1 2 3]1:6 user=> [1 2 3][1 2 3]
在前面声明Hello World和其他函数时,就是用向量来表示函数的参数。注意,(vec)
形式以一个列表为参数,并用这个列表创建向量,而(vector)
形式以多个独立符号为参数,并返回包含它们的向量。
函数(nth)
有两个参数:集合和索引。它跟Java中List
接口的get
方法类似。可以用在向量和列表上,也可以用在Java集合甚至字符串(字符的集合)上,请看下例:
1:7 user=> (nth /'(1 2 3) 1)2
Clojure也支持映射(map,相当于Java的HaspMap
),定义很简单:
{key1 value1 key2 /"value2}
从映射里取值也非常简单:
user=> (def foo {/"aaa/" /"111/" /"bbb/" /"2222/"})#/'user/foouser=> foo{/"aaa/" /"111/", /"bbb/" /"2222/"}user=> (foo /"aaa/")/"111/"
Clojure把前面带冒号的映射键称为“关键字”:
1:24 user=> (def martijn {:name /"Martijn Verburg/", :city /"London/", :area /"Highbury/"})#/'user/martijn1:25 user=> (:name martijn)/"Martijn Verburg/"1:26 user=> (martijn :area)/"Highbury/"1:27 user=> :area:area1:28 user=> :foo:foo
关于关键字,请记住下面这些知识点。
Clojure的关键字是只有一个参数的函数,其参数必须是映射。
在映射上调用这个函数会返回映射里与该关键字函数对应的值。
关键字的使用遵循语法对称性规则,即
(my-map :key)
和(:key my-map)
都是合法的。关键字作为值使用时返回自身。
关键字在使用之前无需声明或
def
。Clojure中的函数也是值,因此可以放在映射里当键用。
可以用逗号(但没必要)来分隔键/值对,因为Clojure会把它们当做空格处理。
除了关键字,其他符号也能用在映射里做键,但关键字太好用了,所以我们要特别提出来,你应该把它用在自己的代码中。
除了映射字面值,Clojure还有个(map)
函数。但不要上当,它不像(list)
,(map)
函数不会产生映射。而是对集合中的元素轮番应用其参数中的函数,并用返回的新值建立一个新集合(实际上是Clojure序列,请参见10.4节)。
1:27 user=> (def ben {:name /"Ben Evans/", :city /"London/", :area /"Holloway/"})#/'user/ben1:28 user=> (def authors [ben martijn])#/'user/authors1:29 user=> (map (fn [y] (:name y)) authors)(/"Ben Evans/" /"Martijn Verburg/")
(map)
还有别的形式,可以一次处理多个集合,但一次输入一个集合的形式最常用。
Clojure也支持集(set),跟Java的HashSet
很像。它的缩写形式是:
#{/"apple/" /"pair/" /"peach/"}
这些数据结构是构建Clojure程序的基础。
Java土著可能会感到吃惊,居然一直没有提到对象。这不是说Clojure不是面向对象的,但它对面向对象的观点的确和Java不一样。Java认为世界是由封装了数据和代码的静态数据类型组成的。而Clojure强调函数和形式,尽管这些在底层都是由JVM上的对象实现的。
Clojure和Java在世界观上的差别最终会体现在代码里。要充分理解Clojure的观点,必须用Clojure写些程序,并弄明白相比Java的面向对象结构它有哪些优势。
10.2.3 数学运算、相等和其他操作
Clojure没有Java里那种意义上的操作符。所以怎么才能,比如说,让两个数相加呢?在Java里这很容易:
3 + 4
但Clojure没有操作符,只能用函数:
(add 3 4)
这也挺好,但我们可以做得更好。因为Clojure里没有操作符,所以我们不用为它们保留任何字符。这就是说Clojure的函数名称可以更加稀奇古怪,所以我们可以这样写1:
(+ 3 4)
Clojure函数一般都支持变参(参数数量可变),比如还可以这样:
(+ 1 2 3)
这个运算结果是6。
1 例子中的(+)
是clojure.core
命名空间下的函数,能够接受0到任意数目的参数,假如没有参数,则返回0。所以虽然Clojure没有操作符,但有很多提供了操作符功能的核心函数,所以你大可不必担心怎么计算3 * 4
,用早已准备好的函数(* 3 4)
就行了。——译者注
Clojure的相等形式(相当于Java里的equals
和==
)状况稍微有点复杂。Clojure有两个跟相等相关的形式:(=)
和(identical?)
。注意它们的名字,这全都是因为Clojure不用为操作符保留字符。另外,(=)
也是等号,而不是赋值符号。
下面这段代码设置了一个列表list-int
和一个向量vect-int
,并比较它们是否相等:
1:1 user=> (def list-int /'(1 2 3 4))#/'user/list-int1:2 user=> (def vect-int (vec list-int))#/'user/vect-int1:3 user=> (= vect-int list-int)true1:4 user=> (identical? vect-int list-int)false
(=)
形式会检查集合是否由相同的对象以相同的顺序组成的( list-int
和vect-int
符合这一要求),而(identical?)
会检查它们是否真的是同一个对象。
你可能也注意到了,符号名称都没有用驼峰式大小写2。这在Clojure中很常见,符号通常都用小写,单词之间用连字符连接。
2 驼峰式大小写(Camel-Case)一词来自Perl语言中普遍使用的大小写混合格式,而Larry Wall等人所著的畅销书Programming Perl: Unmatched power for text processing and scripting(O/'Reilly,2012)的封面图片正是一匹骆驼。——译者注
Clojure中的
true
与false
Clojure中有两个值表示逻辑假:
false
和nil
。其他全是逻辑真。很多动态语言都这样,但对于Java程序员来说这有点奇怪。
掌握了基本的数据结构和操作符,让我们把之前见过的特殊形式和函数拼到一起,写一个稍微长点的Clojure函数吧。