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

《Java程序员修炼之道》10.1 Clojure介绍

关灯直达底部

我们先来看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语法,然后用它写一些真正的程序。