2.1 形式
Clojure具有同像性,也就是说Clojure代码本身,是由Clojure数据构成的。当你运行一段Clojure程序,作为Clojure组成部分的读取器(reader),读入那些被称为“形式”的程序文本块,然后将它们翻译为Clojure的数据结构。接下来,Clojure编译并执行这些数据结构。
表2-1对Clojure中的形式进行了总结。为了理解形式是如何运作的,让我们从一些支持数值类型的简单形式开始。
表2-1 Clojure的形式
2.1.1 使用数值类型
数值字面量本身就是形式。数字会简单的对其自身进行求值。如果在REPL中输入一个数字,他会原样返回给你。
42 -> 42
由数字组成的向量是另外一种形式。下面创建了一个包含数字1、2和3的向量。
[1 2 3] -> [1 2 3]
列表也是一种形式。一个列表可以“仅仅只是数据”,但也可以用于调用函数。下面创建一个列表,它的第1项是一个Clojure函数,例如+号。
(+ 1 2) -> 3
正如你看到的,Clojure对这个列表进行求值的时候,是把它当作了一次调用。与更常见的中缀表示法(infix notation,例如:1+2=3)相对,这种将函数放到最前面的风格被称为前缀表示法(prefix notation)。当然,当函数名是一个单词时,前缀表示法是相当常见的。例如,concat 作为函数名会出现在表达式的起始位置,这就符合大多数程序员的预期。
(concat [1 2] [3 4]) -> (1 2 3 4)
Clojure只不过是像对待所有其他函数一样,把数学运算符也简单地放到起始位置罢了。
采用前缀表示法还有一个实际好处,你是可以很容易将其扩展为任意数量的参数。
(+ 1 2 3) -> 6
甚至在没有参数的这种退化情况(degenerate case)下,它仍然能按照你期望的那样,返回一个零。这非常有利于消除为处理边界条件而产生的特例逻辑,此类逻辑往往非常脆弱。
(+) -> 0
如你所料,Clojure的许多数学和比较运算符,都具有与其他语言相同的名称和语义。加法、减法、乘法、比较和等于操作符都会按照你期望的方式来工作。
(- 10 5) -> 5 (* 3 10 10) -> 300 (> 5 2) -> true (>= 5 5) -> true (< 5 2) -> false (= 5 2) -> false
然而除法可能会令你大吃一惊。
(/ 22 7) -> 22/7
如你所见,Clojure内建了一个比例(Ratio)类型。
(class (/ 22 7)) -> clojure.lang.Ratio
如果你想要的是十进制除法,就得用浮点数的字面量来作为被除数。
(/ 22.0 7) -> 3.142857142857143
如果只需要整数结果,那你可以使用quot和rem函数来获取整型的商和余数。
(quot 22 7) -> 3 (rem 22 7) -> 1
当你需要任意精度的浮点运算时,在数字后面追加一个大写的M,就可以创建一个BigDecimal类型的字面量。
(+ 1 (/ 0.00001 1000000000000000000)) -> 1.0 (+ 1 (/ 0.00001M 1000000000000000000)) -> 1.00000000000000000000001M
为了得到任意精度的整数,可以通过在数字后面追加一个大写的 N,来创建一个BigInt类型的字面量。
(* 1000N 1000 1000 1000 1000 1000 1000) -> 1000000000000000000000N
注意,算式中只需要一个BigInt字面量就够了,因为它会传染到整个计算过程当中。
2.1.2 符号
诸如+、concat和java.lang.String这样的形式都被称为符号,用来为事物命名。例如,用+命名了这样一个函数,它能把一些东西加到一块儿。Clojure 中,符号用来对各式各样的东西命名。
● 函数,例如str和concat。
● 操作符,例如+和-,它们终究不过是函数罢了。
● Java类,例如java.lang.String和java.util.Random。
● 命名空间和Java包,例如clojure.core和java.lang。
● 数据结构和引用类型。
符号不能以数字开头,但可以包含字母、数字、加号(+)、减号(-)、乘号(*)、除号(/)、感叹号(!)、问好(?)、英文句号(.)和下划线(_)。此处列出的是Clojure承诺支持的合法符号的最小字符集。你应该在自己的代码中坚持只使用这些字符,但千万不要假设其他的 Clojure 代码也会如此。Clojure 会采用一些未文档化的其他字符作为其内部符号,另外将来也可能会为符号增加更多的合法字符。请查阅Clojure在线文档,以获取合法符号字符列表的更新。
/和.会被Clojure加以特殊对待,用于支持命名空间,详情参见第2.4.3小节“命名空间”。
2.1.3 字符串与字符
字符串是另外一种读取器形式。Clojure字符串就是Java字符串。它们使用双引号来划定界限,并且可以跨越多行。
"This is a\nmultiline string" -> "This is a\nmultiline string" "This is also a multiline string" -> "This is also\na multiline string"
如你所见,REPL 回显字符串的字面量时,总是包括了换行转义符。如果你确实“打印”了一个多行字符串,那它就会以多行的方式输出。
(println "another\nmultiline\nstring") | another | multiline | string -> nil
Clojure并未封装大多数的Java字符串功能。作为替代,你可以使用Clojure的Java互操作形式来直接调用它们。
(.toUpperCase "hello") -> "HELLO"
toUpperCase前面的句点告知Clojure,应该将其视为一个Java方法,而非Clojure函数。
被Clojure封装了的字符串功能之一是toString。你无需直接调用toString,而是应该使用Clojure的str函数。
(str& args)
str函数与toString有两点不同。一是它能接受多个参数,二是它会跳过nil而不引发错误。
(str 1 2 nil 3) -> "123"
Clojure字符同样也是Java字符。其字面语法是\{letter},letter可以是一个字母,或者下列这些字符的名称:backspace、formfeed、newline、return、space和tab。
(str \h \e \y \space \y \o \u) -> "hey you"
和字符串一样,Clojure 并未封装 Java 的字符处理功能。因此,你同样可以使用Java互操作,比如Character/toUpperCase。
(Character/toUpperCase \s) -> \S
与Java互操作相关的形式,详见2.5节“调用Java”。关于Java字符类Character的更多内容,请参见API文档http://tinyurl.com/java-character。
字符串是由字符组成的序列。当你对字符串调用Clojure的序列处理函数时,你会得到由这些函数返回的一个字符序列。假设你希望通过交错插值的方式,用一段无关的消息来隐藏机密消息。你可以使用interleave函数来混合这两段消息以达成目标。
(interleave "Attack at midnight" "The purple elephant chortled") -> (\A \T \t \h \t \e \a \space \c \p \k \u \space \r \a \p \t \l \space \e \m \space \i \e \d \l \n \e \i \p \g \h \h \a \t \n)
看起来不错,但你可能更希望得到的结果是字符串,而非序列。直接使用str函数把这些字符打包进一个字符串的想法极具诱惑力,但很可惜这是行不通的。
(str (interleave "Attack at midnight" "The purple elephant chortled")) -> "clojure.lang.LazySeq@d4ea9f36"
最主要的问题是,str函数接受的是数量可变的参数,但你传给它的参数只有一个,一个包含了参数列表的序列。解决方案是apply函数。
(apply f args* argseq)
apply函数接受一个函数f、一些可选的args和一个序列argseq作为参数。然后,他会调用f,并将args和argseq解开为一个参数列表传给f。下面使用(apply str ...)由字符序列来创建一个字符串。
(apply str (interleave "Attack at midnight" "The purple elephant chortled")) -> "ATthtea cpku raptl em iedlneipghhatn"
你还可以再次使用(apply str ...),显示那条机密消息。
(apply str (take-nth 2 "ATthtea cpku raptl em iedlneipghhatn")) -> "Attack at midnight"
调用(take-nth 2 ...),会从序列中依次剔除每第2个元素,这样就还原了被混淆的消息。
2.1.4 布尔值与nil
Clojure的布尔值规则很容易理解。
● true为真,false为假。
● 除了false,在进行布尔求值的上下文中,nil也为假。
● 除了false和nil,其他任何东西在布尔求值的上下文中都为真。
Lisp程序员们请注意:在Clojure中,空列表不为假。
; (if部分) (else部分) (if () "We are in Clojure!" "We are in Common Lisp!") -> "We are in Clojure!"
下面这个警告是针对C程序员的:在Clojure中,零也不为假。
; (if部分) (else部分) (if 0 "Zero is true" "Zero is false") -> "Zero is true"
谓词(predicate)是一种返回true或false的函数。作为Clojure惯例,给谓词命名时皆以问号结尾,例如true?、false?、nil?和zero?。
(true? expr) (false? expr)
(nil? expr) (zero? expr)
true?用于测试值是否确实为 true,而不仅仅是在布尔求值的上下文中为真。唯一能通过true?测试的只有true本身。
(true? true) -> true (true? "foo") -> false
nil?和false?也是一样。只有nil能通过nil?的测试,也只有false能通过false?的测试。zero?可以对任何一种数值类型使用,如果值为零则返回true。
(zero? 0.0) -> true (zero? (/ 22 7)) -> false
更多Clojure中的谓词。可在REPL中输入(find-doc #"\?$")来查看它们。
2.1.5 映射表、关键字和记录
Clojure中,映射表是一种由键值对组成的容器,使用一对花括号作为其字面表示法。你可以使用映射表字面量来创建一个由编程语言发明人组成的查询表。
(def inventors {"Lisp" "McCarthy" "Clojure" "Hickey"}) -> #'user/inventors
值“McCarthy”被关联至键“Lisp”,同时值“Hickey”被关联至键“Clojure”。
如果想增强它的可读性,你可以使用逗号来分割每组键值对。Clojure对此毫不在意,它会把逗号视为空格符。
(def inventors {"Lisp" "McCarthy", "Clojure" "Hickey"}) -> #'user/inventors
映射表同时也是函数。如果你把键作为参数传给映射表,那么它会返回与这个键对应的值,或是当键不存在时直接返回nil。
(inventors "Lisp") -> "McCarthy" (inventors "Foo") -> nil
你也可以使用更繁琐一些的get函数。
(get the-map key not-found-val?)
get函数允许你为缺失的键指定一个默认返回值。
(get inventors "Lisp" "I dunno!") -> "McCarthy" (get inventors "Foo" "I dunno!") -> "I dunno!"
由于Clojure 的数据结构是不可变的,并且都正确的实现了hashCode,所以任意一种Clojure数据结构都可以用做映射表的键。其中,Clojure关键字是一种相当常见的键类型。
除了是以冒号(:)起头之外,关键字的其他部分与符号非常相似。看下面,关键字会解析为它们自身。
:foo -> :foo
这是它们与符号之间最大的不同,符号总是会引用某种东西。
foo -> CompilerException java.lang.RuntimeException: Unable to resolve symbol: foo in this context
关键字会被解析为他们自身的这个特点,非常有利于令其成为映射表的键。你可以使用关键字作为键,来重新定义前面的那个发明人映射表。
(def inventors {:Lisp "McCarthy" :Clojure "Hickey"}) -> #'user/inventors
关键字同样也是函数。它们接受一个映射表作为参数,并在该映射表中查找其自身。只要把关键字和inventors的位置相互调换,你就可以通过调用映射表函数或关键字函数来查找语言的发明人了。
(inventors :Clojure) -> "Hickey" (:Clojure inventors) -> "Hickey"
在调用诸如引用(第5章)和代理API那样的高阶函数时,这种灵活性就能大派用场。
如果有好几个映射表都拥有相同的键,那么你可以用 defrecord 创建一种记录类型,对这一事实加以描述(同时也是强化)。
(defrecord name [arguments])
此处的参数名称会转换为键。这样当创建一条记录时,就需要传入相对应的值。下面使用defrecord创建了一种记录类型Book。
(defrecord Book [title author]) -> user.Book
接下来,你就可以用user.Book来实例化一条记录了。
(->Book "title" "author")
一旦你实例化了一个Book,你会发现它的行为与其他映射表简直像极了。
(def b (->Book "Anathem" "Neal Stephenson")) -> #'user/b b -> #:user.Book{:title "Anathem", :author "Neal Stephenson"} (:title b) -> "Anathem"
记录也可以使用别的方法来实例化。最初的语法你应该已经见过了。
(Book. "Anathem" "Neal Stephenson") -> #user.Book{:title "Anathem", :author "Neal Stephenson"}
你也可以采用字面语法来实例化一条记录。要这么做,只要照原样输入你在REPL中看到的返回内容即可。你会发现与前面唯一的区别是,记录的字面量必须使用全限定名。
#user.Book{:title "Infinite Jest", :author "David Foster Wallace"} -> #user.Book{:title "Infinite Jest", :author "David Foster Wallace"}
到目前为止,你已经见过了数值字面量、列表、向量、符号、字符串、字符、布尔值、记录和nil。其余的那些Clojure形式,会在需要时,在本书的后续章节中加以讨论。作为参考,你可以查阅表2-1“Clojure的形式”,那儿列出了所有本书中会用到的Clojure形式,包括简短的例子和完整讨论的章节指引。