1.3 探索Clojure的程序库
Clojure 代码通常都被打包在程序库中。每个 Clojure 库都属于某个命名空间,这与Java的包非常类似。你可以通过require来加载一个Clojure库。
(require quoted-namespace-symbol)
当你使用require加载了一个名为clojure.java.io的库时,Clojure会在CLASSPATH中查找名为clojure/java/io.clj的文件。试试看。
user=> (require 'clojure.java.io)
-> nil
起头的单引号(')是必不可少的。它表示对库名的引用(关于引用的内容参见2.2节“读取器宏”)。返回nil表示库加载成功。如果你想测试一下,可以加载本章的示例代码examples.introduction。
user=> (require 'examples.introduction)
-> nil
examples.introduction 库中包含了一个 Fibonacci 数列的实现,这是函数式编程语言传统的“Hello World”程序。在4.2节“怎样偷个懒”中,我们会探索关于Fibonacci数列的更多细节。现在,你只要确保能够执行这个示例函数fibs即可。你可以在REPL中输入下面的代码行以获取前10个Fibonacci数。
(take 10 examples.introduction/fibs) -> (0 1 1 2 3 5 8 13 21 34)
如果你看到的前10个Fibonacci数与此处列出的相同,说明你已经成功安装了本书的示例。
本书的所有示例都进行了单元测试,测试代码位于examples/test目录。本书并未明确包含这些示例的测试本身,但你会发现它们可以作为非常有用的参考。你也可以自己执行lein test命令来运行这些单元测试。
1.3.1 require和use
当你用require加载了一个Clojure程序库,就需要使用命名空间限定的名称来引用这个库里面的内容。你必须这么写:examples.introduction/fibs,而不是简单地写下fibs了事。下面,确保你新启动了一个REPL,然后试着输入。
(require 'examples.introduction) -> nil (take 10 examples.introduction/fibs) -> (0 1 1 2 3 5 8 13 21 34)
总要使用完全限定名实在是太啰嗦了。你可以对命名空间使用refer,将其下的所有名称映射到你的当前命名空间中。
(refer quoted-namespace-symbol)
对examples.introduction调用refer,然后确认你能否直接调用fibs了。
(refer 'examples.introduction) -> nil (take 10 fibs) -> (0 1 1 2 3 5 8 13 21 34)
为方便起见,Clojure的use函数把require和refer合并成了一个步骤。
(use quoted-namespace-symbol)
在新启动的REPL中,你应该能够顺利执行下列代码。
(use 'examples.introduction) -> nil (take 10 fibs) -> (0 1 1 2 3 5 8 13 21 34)
在使用本书的示例代码期间,你可以在调用require或use时,使用:reload标记用来强制重新加载一个程序库。
(use :reload 'examples.introduction) -> nil
如果你进行了一些修改,想看看结果如何,但又不想重新启动REPL,那么:reload标记就能帮上大忙。
1.3.2 查找文档
通常情况下,你都可以在REPL中找到你需要的文档。最基本的辅助函数是doc。
(doc name)
试试使用doc来打印str函数的文档。
user=> (doc str)
-------------------------
clojure.core/str
([] [x] [x & ys])
With no args, returns the empty string. With one arg x, returns
x.toString(). (str nil) returns the empty string. With more than
one arg, returns the concatenation of the str values of the args.
doc 输出的第一行包含了目标函数的全限定名称。接下来的一行包含了可能的参数列表,这是直接从代码中生成的。“参数命名惯例”中有一些常用的参数名称,及它们的用途解释。最后,剩下的那几行是这个函数的文档字符串(doc string),当然,如果在函数定义中包含了它们的话。
你可以为自己的函数添加文档字符串,只要将它放置在紧接着函数名的位置即可。
src/examples/introduction.clj (defn hello "Writes hello message to *out*. Calls you by username" [username] (println (str "Hello, " username)))
有时你想要查阅文档时,不清楚目标的确切名称。find-doc 会用你传入的正则表达式或是字符串,找出所有那些调用doc函数得到的输出能与之匹配的东西。
(find-doc s)
试试用find-doc检索一下Clojure是如何进行reduce的。
user=> (find-doc "reduce") ------------------------- clojure/areduce ([a idx ret init expr]) Macro ... details elided ... ------------------------- clojure/reduce ([f coll] [f val coll]) ... details elided ...
reduce用于对Clojure容器进行归纳,第3.2.4小节“序列转换”中讨论了该话题。areduce则作用于Java数组,第9.4.2小节“使用Java容器”中有关于它的讨论。
参数命名惯例
reduce和areduce的文档字符串展示了几个简练的参数名称。表1-1列出了一些约定的参数名,以及通常情况下应该如何使用它们。
表1-1 参数名
这些名称看起来似乎过于简练了,但采用它们有一个很好的理由:“好名称”往往已经被Clojure函数给占用了!尽管从语法角度,参数可以和其他函数重名,但这是一种糟糕的风格:参数会遮蔽函数,当它们同处一室时,后者将会失效。所以,千万不要把你的引用叫做ref、把代理叫做agent,或者把数量叫做count。这些名称代表的是函数。
Clojure 自己的源码中,很多都是用 Clojure 本身写成的,阅读这些源码极具启发性。你可以使用repl库的source函数来查阅某个Clojure函数的源码。
(clojure.repl/source a-symbol)
查阅一下简单的identity函数源码看看。
(use 'clojure.repl) (source identity) -> (defn identity "Returns its argument." {:added "1.0" :static true} [x] x)
当然,你也可以使用Java的反射API。用诸如class、ancestors和instance?这样的方法,来反射其底层的Java对象模型。例如,Clojure的容器同样也是Java容器。
(ancestors (class [1 2 3])) -> #{clojure.lang.ILookup clojure.lang.Sequential java.lang.Object clojure.lang.Indexed java.lang.Iterable clojure.lang.IObj clojure.lang.IPersistentCollection clojure.lang.IPersistentVector clojure.lang.AFn java.lang.Comparable java.util.RandomAccess clojure.lang.Associative clojure.lang.APersistentVector clojure.lang.Counted clojure.lang.Reversible clojure.lang.IPersistentStack java.util.List clojure.lang.IEditableCollection clojure.lang.IFn clojure.lang.Seqable java.util.Collection java.util.concurrent.Callable clojure.lang.IMeta java.io.Serializable java.lang.Runnable}
http://clojure.github.com/clojure有完整的Clojure API在线文档。右侧栏按照名称对所有的函数和宏建立了链接,左侧栏则链接至一系列描述Clojure的特性的文章。