本节书摘来自异步社区《Clojure程序设计》一书中的第1章,第1.2节Clojure编程快速入门,作者 【美】Stuart Halloway , Aaron Bedra,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.2 Clojure编程快速入门
Clojure程序设计
要运行Clojure及本书的示例代码,你需要两件东西。
Java运行时。请下载1并安装Java 5或是更高版本。Java 6具有显著的性能提升和更好的异常报告,如果可能就尽量选它吧。
Leiningen2。Leiningen是一个用于管理依赖项的工具,并且可以基于你的代码启动各种任务。在Clojure世界中,它是处理这项工作最常用的工具了。
你将会使用Leiningen来安装Clojure和本书所有示例代码的依赖项。如果你已经安装了Leiningen,那就应该已经熟悉相关的基础知识了。否则,你应该快速的浏览一下Leiningen的GitHub主页3,在那儿你能找到如何安装,以及其基本用法的说明。现在还不用急着去学习所有这一切,因为本书会指引你轻松地掌握那些必须的命令。
在阅读本书期间,请使用与本书示例代码匹配的Clojure版本。读完本书后,你就可以按照“自行构建Clojure”中的说明,构建一个最新鲜的Clojure版本。
自行构建Clojure
你可能希望从源码构建Clojure,以获得最新的特性和bug修复。这里是具体做法。
git clone git://github.com/clojure/clojure.git
cd clojure
mvn package
本书的示例代码会经常更新,与匹配Clojure当前最新的开发版本。请检查示例代码目录中的README文件,里面有最近一次测试通过时,对应的Clojure修订版本号。
请参阅前言中“下载示例代码”中说明,下载本书的示例代码。当你下载了示例代码后,你还需要使用Leiningen获取它们的依赖项。请在示例代码的根目录下执行。
lein deps
那些依赖项将被下载到本地并放置在适当的位置。为了测试安装是否正确,你可以进入到放置示例代码的目录,并启动一个Clojure的REPL(读取-求值-打印循环)。Leiningen包含了一个启动REPL的脚本,它可以连同依赖项一起加载Clojure,本书后面部分会用到那些依赖项。
lein repl
当你成功的启动了REPL,它将会显示“user=>”对你进行提示。
Clojure
user=>
现在,你已经为“Hello World.”做好了准备。
1.2.1 使用REPL
来看看如何使用REPL,让我们创建几个“Hello World”的变体。首先,在REPL提示符下键入(println "hello world")。
user=> (println "hello world")
->hello world
第二行的“hello world”,就是REPL针对你提交的请求,产生的控制台输出。
接下来,将你的“Hello World”封装成一个函数,让它可以通过名字向人问好。
(defn hello [name](str "Hello, " name))
-> #'user/hello
我们来分析一下。
defn定义了一个函数。
hello是这个函数的名称。
hello函数接受一个参数name。
str是一个函数调用,把由任意参数组成的列表连接为一个字符串。
defn、hello、name和str都是符号(symbols),代表了它们各自涉及事物的名称。在第2.1.2小节“符号”中有关于合法符号的定义。
再看看这行代码的返回值:#'user/hello。前缀#'表示这个函数是用一个Clojure变量(var)来保存的,其中user是这个函数所在的命名空间(namespace)(就像Java的默认包一样,user是REPL的默认命名空间)。你现在还不必为变量和名字空间担忧,第2.4节“变量、绑定和命名空间”里有关于它们的讨论。
你现在可以调用hello函数,并传入你的名字了。
user=> (hello "Stu")
-> "Hello, Stu"
如果你发现REPL的状态令你倍感困惑,最简单的解决办法就是直接关闭这个REPL(Windows下使用CTRL+C,*nix下则是CTRL+D),然后再另外启动一个。
1.2.2 特殊变量
REPL包括几个有用的特殊变量。当你使用REPL时,最近三次求值结果的描述被分别存储在特殊变量1、2和*3中。这使得进行迭代变的非常容易。下面,让我们向几个不同的名字问声好。
user=> (hello "Stu")
-> "Hello, Stu"
user=> (hello "Clojure")
-> "Hello, Clojure"
现在,你可以使用那几个特殊变量,把最近的几个工作成果组合起来。
(str *1 " and " *2)
-> "Hello, Clojure and Hello, Stu"
如果你在使用REPL的过程中犯了错,你会看到一个Java异常。出于简洁方面的考虑,细节往往被省略了。例如,除以零是不允许的。
user=> (/ 1 0)
-> ArithmeticException Divide by zero clojure.lang.Numbers.divide
这是个显而易见的问题,但有些时候问题会更加微妙,这时你就需要获得更详细的堆栈跟踪(stack trace)信息了。最后一个异常被保存在特殊变量*e中。由于Clojure异常就是Java异常,所以你能使用pst函数4(print stacktrace)得到堆栈跟踪信息。
user=> (pst)
-> ArithmeticException Divide by zero
| clojure.lang.Numbers.divide
| sun.reflect.NativeMethodAccessorImpl.invoke0
| sun.reflect.NativeMethodAccessorImpl.invoke
| sun.reflect.DelegatingMethodAccessorImpl.invoke
| java.lang.reflect.Method.invoke
| clojure.lang.Reflector.invokeMatchingMethod
| clojure.lang.Reflector.invokeStaticMethod
| user/eval1677
| clojure.lang.Compiler.eval
| clojure.lang.Compiler.eval
| clojure.core/eval
更多与Java互操作方面的内容,参见第9章“极尽Java之所能”。如果你的代码块实在太大,不便于在REPL中逐行敲入,不妨将代码保存到一个文件中,然后通过REPL,使用绝对路径或是相对路径(相对于启动REPL的路径)来加载这个文件。
; 保存一些东西到temp.clj中, 然后执行...
user=> (load-file "temp.clj")
REPL是一个美妙的场所,在这里你可以尝试各种想法并立即获得反馈。为达到最佳效果,阅读本书时,请务必保持随时都开启着REPL。
1.2.3 添加共享状态
上一节中的hello函数是“纯粹的”,也就是说,它不会产生任何副作用。纯函数易于开发、测试,并易于理解,你应该优先选择它们来处理任务。
可是,大多数程序拥有共享状态,并且需要使用非纯粹的函数来管理这些共享状态。让我们对hello函数进行扩展,使其能够追踪过往访客的足迹。首先,你需要一种数据结构来追踪访客。集合就非常合适。
{}
-> {}
{}是空集合的字面表示法。接下来,你需要conj函数。
(conj coll item)
conj是conjoin(连接)的缩写,它会新建一个含有新增项的集合。将元素连接到集合,就好像是创建了一个新的集合。
(conj #{} "Stu")
-> {"Stu"}
现在你可以创建新的集合了,但你还需要某种方法来对当前访客的集合保持跟踪。为此,Clojure提供了几种引用类型。最基本的引用类型是原子。
(atom initial-state)
你可以使用def来为你的原子命名。
(def symbol initial-value?)
def有点像defn,但更为通用。Def既能定义函数,又能定义数据。下面使用atom创建一个原子,并用def将这个原子绑定到名称visitors上。
(def visitors (atom #{}))
-> #’user/visitors
要更新一个引用,你需要使用诸如swap!这样的函数。
(swap! r update-fn & args)
swap!会对拿引用r去调用update-fn,并根据需要传递其他可选的参数。下面试一下用conj作为更新函数,把一个访客swap!进入到访客集合中。
(swap! visitors conj "Stu")
-> #{"Stu"}
原子只是Clojure的几种引用类型之一。选择恰当的引用类型时,需要格外小心仔细(相关讨论参见第5章“状态”)。
你可以在任何时候使用deref或者它的缩写@号来提取引用内部的值。
(deref visitors)
-> #{"Stu"}
@visitors
-> #{"Stu"}
现在,是时候创建这个更加复杂的新版hello了。
src/examples/introduction.clj
(defn hello
"Writes hello message to *out*. Calls you by username.
Knows if you have been here before."
[username]
(swap! visitors conj username)
(str "Hello, " username))
下一步,检查一下看能否在内存中正确地追踪了。
(hello "Rich")
-> "Hello, Rich"
@visitors
-> #{"Aaron" "Stu" "Rich"}
你的访客列表十有八九与此处显示的不同。这就是状态捣的乱!结果是否会有差别,取决于事情何时发生。你还可以据此推论出一个函数是否管理着本地信息。对状态进行推断,需要对其演变历史有着充分的认识。
只要可能,就应该极力避免状态。但是当你确实需要它的时候,通过使用诸如原子这样的引用类型,就能让状态保持完整以及可控。原子(和所有其他的Clojure引用类型)对于多个线程和多个处理器都是安全的。更棒的是,获得这种安全性无需借助声名狼藉的锁定机制,那实在是太让人抓狂了。
至此,你应该已经能很舒畅的在REPL中录入那些较短的代码了。其实那些较长的代码也没有太多不同,你同样可以在REPL中加载并运行大量有成百上千行代码的Clojure库。下面让我们来探索一下吧。
1 http://www.oracle.com/technetwork/java/javase/downloads/index.html。
2 http://github.com/technomancy/leiningen。
3 http://github.com/technomancy/leiningen。
4 pst函数仅适用于Clojure1.3.0及更高版本。