我们先来看Clojure跟Java在理念上最重要的差别,即对状态,变量和存储的不同认识。如图10-1所示,Java(跟Groovy和Scala一样)有一个内存和状态模型,把变量当作保存可变内容的“盒子”(内存位置)。
图10-1 命令式语言的内存使用
而Clojure认为值才是真正重要的概念。值可以是数字、字符串、向量、映射、集合,或其他任何东西。一旦创建,值就再也不会改变。这一点真的非常重要,所以我们要再重复一次。一旦创建,Clojure的值就不能再变了,因为它们是不可变的。
这就是说命令式语言那种装着可变内容的盒子模型不是Clojure思考问题的方式。图10-2是Clojure处理状态和内存的方式。它在名字和值之间创建了一个关联关系。
图10-2 Clojure的内存使用
这就是绑定,通过特殊形式(def)
建立。Clojure中的特殊形式相当于Java的关键字,但请注意,Clojure中的术语“关键字”含义不同,稍后我们会介绍。
(def)
的句法是:
(def<名称> <值>)
如果你觉得这个句法看起来有点怪异,不要担心,这完全是Lisp的普通句法,你很快就会习惯的。现在你可以假装是在调用下面这样一个方法,只是括号的位置不太一样:
def(<名称>, <值>)
接下来我们要在Clojure的交互式环境中写一个久经考验的例子,演示一下(def)
的用法。
10.1.1 Clojure的Hello World
如果你还没装Clojure,请参见附录D。然后切换到Clojure所在的目录,运行如下命令:
java -cp clojure.jar clojure.main
这个命令会启动Clojure的REPL环境。在编写Clojure代码时,你会在这个交互环境里花上很多时间。
user=>
是Clojure的会话提示符,你可以把这个会话环境当做高级的调试环境,或者命令行工具:
user=> (def hello (fn /"Hello world/"))#/'user/hellouser=> (hello)/"Hello world/"
这段代码一开始先给标识符hello
绑定一个值。(def)
就是用来建立标识符(Clojure称之为符号)和值之间的绑定关系的。底层实现的时候,它也会创建一个对象var
,用来表示这种绑定关系(和符号的名字)。
那这里绑定的值是什么?这个值是:
(fn /"Hello world/")
这是一个函数,在Clojure中也是一个纯正的值(因此也是不可变的)。这个函数没有参数,返回字符串/"Hello world/"
。
绑定之后,可以用(hello)
执行。Clojure运行时会输出该函数的计算结果,也就是/"Hello world/"
。
现在,应该录入这个例子(如果你还没做),看看它的表现是不是跟我们说的一样。完成之后,我们就可以继续探索了。
10.1.2 REPL入门
在REPL中可以输入Clojure代码,也可以执行Clojure函数。它是个交互式环境,而且在前面得出的计算结果不会被丢掉。可以用它做探索式编程,我们会在10.5.4节讨论这种编程方式,基本就是不断试验代码。用Clojure开发经常都是先在REPL里把代码调好,然后用正确的构件搭出越来越大的函数。
马上看一个例子。先声明,再次调用def
可以改变符号和值的绑定关系,我们在REPL中看一下。代码中用的实际上是(def)
的变体(defn)
:
user=> (hello)/"Hello world/"user=> (defn hello /"Goodnight Moon/")#/'user/hellouser=> (hello)/"Goodnight Moon/"
注意,hello
最初的绑定关系一直都在,直到被你改掉,这是REPL的一个关键特性。这还是状态,只不过换了个说法,变成了哪个符号绑定到哪个值上,并且这个状态存在于用户输入的不同行间。
Clojure中没有可变状态,但有可以改变绑定值的符号。Clojure不是让“内存盒子”中的内容改变,而是让符号绑定到不同的不可变值上。换句话说就是在程序的生命期内,var
可以指向不同的值。请参见图10-3。
图10-3 可以改变的Clojure绑定
注意 可变状态和不同绑定两者之间的区别很微妙,但这个概念很重要,一定要掌握。要记住, 可变状态是指盒子中的内容变了,而重新绑定是指在不同时间指向不同的盒子。
这段代码中还溜进了另一个Clojure概念,“定义函数”宏(defn)
。宏是类Lisp语言的关键概念之一,其核心思想是内置结构和普通代码之间的区别应该尽可能小。
用宏可以创建跟内置语法类似的形式。创建宏是高级话题,但掌握了它之后,你就能制造出非常强大的工具。
这就是说语言真正的原语系统(特殊形式)可以用一种几乎无法察觉的方式构建起整个语言的核心。宏(defn)
就是这种构建的产物。它只是将函数值绑定到符号的相对简单的方法(当然,要创建合适的var
)。
10.1.3 犯了错误
如果你犯错了,会怎么样?比如你漏掉了(函数声明的一部分,表明这个函数没有参数)。
user=> (defn hello /"Goodnight Moon/")#/'user/hellouser=> (hello)java.lang.IllegalArgumentException: Wrong number of args (0) passed to:user$hello (NO_SOURCE_FILE:0)
所有后果只是hello
标识符绑定到了一个未知的东西上。你可以在REPL中重新绑定来修复它:
user=> (defn hello (println /"Dydh da an Nor/")) ; /"Hello World/" in Cornish#/'user/hellouser=> (hello)Dydh da an Norniluser=>
跟你猜的一样,上面这段代码中的分号(;
)表示直到行尾的内容都是注释,(println)
是输出字符串的函数。注意看(println)
,它跟所有函数一样,返回了一个值,在函数执行结束后回显到REPL中。结果值是nil
,相当于Java里的null
。
10.1.4 学着去爱括号
奇思妙想和幽默感是程序员文化不可或缺的一部分。说Lisp是“很多烦人的傻括号”的缩写就是个很古老的笑话。其实Lisp是列表处理(List Processing)的缩写,真相就是这么平淡无奇。很多Lisp程序员都用这个笑话自嘲,因为它确实戳到了Lisp语法的痛处。
实际上,这个障碍被夸大了。Lisp句法的确特立独行,但也不像看起来那么碍手碍脚。另外,Clojure还为减轻入门的障碍做了几项创新。
我们再看一下Hello World。调用返回“Hello World”的函数写成:
(hello)
用Java写应该是这样(假设你已经在某个类里定义了hello
方法):
hello;
但Clojure的表达式不是myFunction(someObj)
,而是(myFunction someObj)
。这种写法叫波兰表示法,因为它是19世纪的波兰数学家发明的。
如果你研究过编译原理,可能想知道这是否和抽象语法树(AST)之类的概念有关。简单地说是“有”。可以证明,用波兰表示法(Lisp程序员通常管它叫s表达式)写成的Clojure或其他Lisp程序是其简单直接的AST表示。
你可以认为Lisp程序是直接用AST写的。Lisp程序的数据结构表示和代码没有本质上的差别,所以代码和数据是完全可以互换的。这也是Clojure的表示法看起来有点奇怪的原因——类Lisp语言用它来模糊内置的原生代码、用户代码和类库代码之间的区别。对于Java程序员来说,这股强大力量对他们的引力要远远超过稍微有点古怪的语法。
让我们更深入地学一些Clojure语法,然后用它写一些真正的程序。