首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》10.2 寻找Clojure:语法和语义

关灯直达底部

我们上一节介绍了(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-intvect-int符合这一要求),而(identical?)会检查它们是否真的是同一个对象。

你可能也注意到了,符号名称都没有用驼峰式大小写2。这在Clojure中很常见,符号通常都用小写,单词之间用连字符连接。

2 驼峰式大小写(Camel-Case)一词来自Perl语言中普遍使用的大小写混合格式,而Larry Wall等人所著的畅销书Programming Perl: Unmatched power for text processing and scripting(O/'Reilly,2012)的封面图片正是一匹骆驼。——译者注

Clojure中的truefalse

Clojure中有两个值表示逻辑假:falsenil。其他全是逻辑真。很多动态语言都这样,但对于Java程序员来说这有点奇怪。

掌握了基本的数据结构和操作符,让我们把之前见过的特殊形式和函数拼到一起,写一个稍微长点的Clojure函数吧。