Clojure程序设计

978-7-115-30847-4
作者: 【美】Stuart Halloway Aaron Bedra
译者: 温瑞云
编辑: 陈冀康

图书目录:

详情

本书是介绍Clojure编程语言和程序设计的经典之作。第2版针对Clojure 1.3进行了全面的更新。全书共包括10章,分别介绍了Clojure概览和基本特性、序列和函数式编程、并发编程模型、协议和数据类型、宏、多重方法以及Clojure对Java的调用。最后提供了一个完整了解Clojure应用开发全过程的实例。

图书摘要

Clojure程序设计

【美】 Stuart Halloway Aaron Bedra 著

温瑞云 译

人民邮电出版社

北京

Clojure是一种基于Java虚拟机的动态编程语言。它功能强大,支持函数式编程,简化了并发编程,并且能调用Java程序。正是这些优点,使其赢得了众多程序员的青睐。

本书是介绍Clojure编程语言和程序设计的经典之作。第 2版针对Clojure 1.3进行了全面的更新。全书共包括 10 章,分别介绍了 Clojure 概览和基本特性、序列和函数式编程、并发编程模型、协议和数据类型、宏、多重方法,以及Clojure对Java的调用。最后提供了一个完整了解Clojure应用开发全过程的实例。

本书适合有不同语言背景而想要学习和了解Clojure编程的程序员阅读。函数式程序员、Java和C#程序员、Lisp程序员,以及Perl、Python和Ruby程序员,都能够通过阅读本书得到不同程度的收获。

献给Craig Bedra,我的父亲和导师。

是你让我懂得了通过探索进行学习的价值,

并且让我明白诸如魔法一类的事情是不存在的。

——Aaron

有许多人为本书那些精彩的部分做出了贡献。残留的问题和错误完全是由我们造成的。

感谢Relevance和Clojure/core这两个了不起的团队,是你们创造性的氛围,让好点子得以茁壮成长。

感谢Clojure邮件列表里友善的人们,是你们给予了我们莫大的帮助和鼓励。

注释:① http://groups.google.com/group/clojure。

感谢 Pragmatic Bookshelf的所有人。尤其是我们的编辑Michael Swaine,为我们有些狂妄的交付时间表提供了很好的建议。感谢Dave Thomas和Andy Hunt,你们创建了一个有趣的技术书籍编写平台,它令书籍作者热情满怀。

感谢所有向本书勘误表页面投递建议的人们。

注释:② http://www.pragprog.com/titles/shcloj2/errata。

感谢技术评审们的意见和建议,你们是Kevin Beam、Ola Bini、Sean Corfield、Fred Daoud、Steven Huwig、Tibor Simic、David Sletten、Venkat Subramaniam和Stefan Turalski。

特别要感谢David Liebke,是你编写了第 6章“协议和数据类型”的最初版本。如果不是因为你用全新的思路提供了一份奇妙的指南,这本书不会成为现在的样子。

感谢Rich Hickey,是你创造了Clojure这种杰出的编程语言,并培育出围绕着它的社区。

感谢我的妻子 Joey,我的女儿Hattie、Harper和Mabel Faire。是你们让我每天都如沐阳光。

——Stuart

感谢我的妻子,Erin,是你给予了我无尽的爱与鼓励。

——Aaron

我们正在被复杂性淹没。其中绝大部分是偶然复杂性——并非源自问题本身,而是源自于我们为了解决问题而采用的方法。面向对象编程看起来容易,但生产出来的程序,往往会成为一张由可变对象交织而成的复杂巨网。单单是因为调用了一个对象上的方法,就可能会引起遍及整个对象关系图的一连串变化。在这种情况下,想要理解何时将会发生何事,事物是如何进入某种状态的,或是为修正某个bug而试图让对象回到特定状态,都变得极为复杂。而且一旦混入了并发问题,事情很快就会变得无法收拾。我们对程序使用了仿制对象(mock objects)和测试套件(test suites),但依然收效甚微。这不得不让我们对手头的工具和编程模型产生质疑。

函数式编程提供了别的选择。通过强调纯函数——传入和返回的都是不可变值(immutable value),副作用的存在成为了特例,而不再是常态。在多核架构中,我们面临的并发问题日益增加。因此,这种特性只会变得更加重要。Clojure的设计目标,就是要让函数式编程更加平易近人,且兼备商业软件开发者所需的实用性。首先要能运行在像Java虚拟机这样可靠的基础设施之上,还要能支持现有客户在Java框架及库方面的投资。Clojure非常清楚满足上述两点的必要性,以及这么做能带来的巨大实用性。

这是一种面向专业开发者(Stuart自己就是)的编程语言。Stuart的这本书之所以让人如此激动,源于他对Clojure的把握。很明显,他对Clojure瞄准的痛处有着充分的体验,这也增强了本书务实的态度。阅读本书,就像是一次充满激情的旅行,以实际应用为基础,逐步介绍Clojure的关键特性——有可能是一些全新的概念。我希望它能鼓舞你运用Clojure去编写软件,然后,回过头来你会说:“我不仅仅完成了工作,而且没想到采用的方法竟然如此健壮和简单,最棒的是,编写Clojure代码实在是太有乐趣了!”。

——Rich Hickey

Clojure之父

自本书第1版发行之后,很多东西发生了改变。是的,Clojure语言本身得以增强,例如协议(protocol)和记录(record)的引入。然而,最有意义的是,Clojure已应用于各种不同的领域。人们正在使用Clojure建立新系统,分析大规模数据集,以及处理通信、金融、互联网和数据库方面的工作。同时,一个大型的、乐于助人的社区已经围绕着Clojure成长起来。随之而来,涌现出了大量的程序库。这些库的出现格外令人激动,不仅仅在于它们提供的功能设施;还在于它们当中最优秀的那些库,欣然接受了Clojure倡导的方法和机制,从而在简洁性和互操作性方面达到了全新的高度。

Stuart和Aaron确保在第2版中覆盖了语言的新增功能,并尝试着借助一些来自社区的库,来演示这些功能是如何运作的。本书仍然提供了令人振奋的Clojure简介,我希望它能激发你加入我们的社区,并最终对Clojure生态系统做出贡献。

——Rich Hickey

Clojure之父

Clojure是一种基于 Java虚拟机(JVM,Java Virtual Machine)的动态编程语言(dynamic programming language)。它具有以下引人注目的特性。

· Clojure非常优雅。

摒弃了杂乱累赘的语法束缚,Clojure 干净、仔细的设计使你在编写代码时总能立刻切入问题的本质。

· Clojure是Lisp的再度崛起。

Clojure从Lisp继承了强大的力量,却未受到Lisp历史的束缚。

· Clojure是一种函数式语言(functional language)。

作为一门函数式语言,Clojure的数据结构具有不可变性(immutable),且大多数函数没有副作用(side effect)。因此,编写正确的程序更加容易,也能更轻松地将小程序组合成一个大家伙。

注释:① 译注:初次接触函数式编程的读者可能对何为“副作用”略感疑惑。所谓副作用,就是指某个函数执行期间导致系统状态发生了变化。我们常见的大多数语言都有副作用。而典型的函数式语言没有赋值语句,也就不会对包括全局变量、函数参数或局部变量在内的各种系统状态造成影响。

· Clojure简化了并发编程。

很多其他语言围绕同步锁(locking)建立的并发模型,难以驾驭。为此,Clojure提供了数个锁机制的替代方案:软事务内存(Software Transactional Memory, STM)、代理(agent)、原子(atom)和动态变量(dynamic variable)。

注释:② 译注:本书中后续章节中还将出现另外一个含义的代理(Proxy),但绝大多数情况下,根据上下文即可分辨文中出现的“代理”应该是Agent还是Proxy,在可能会引起误解的地方,译者会加以注明。

· Clojure与 Java彼此亲密无间。

在Clojure中调用Java代码,无需任何中间的转换层,直接而且快速。

· 不同于许多其他流行的动态语言,Clojure运行飞快。

Clojure的实现利用了现代Java虚拟机上的众多优化技术。

尽管许多其他语言也包含了上述诸多特性中的一部分,但与它们相比,Clojure仍显得魅力非凡。上述任何一个特性,都极为强大和有趣。Clojure的迷人之处在于,将这些特性以非常干净的方式融合在了一起,且做到彼此协作无间。本书的第1章“启航”,将介绍以上这些特性及更多的内容。

谁应该阅读本书

Clojure 是一种强大的通用型(general-purpose)编程语言。如果你是一名经验老到的程序员,具备类似C#、Java、Python或者Ruby这样的现代编程语言的开发经验,并正在寻找更为强大、更加优雅的编程语言,那么本书是为你量身定做的。

Clojure构建于Java虚拟机之上,并且运行飞快。如果你是一名对表现力丰富的动态语言馋涎已久,但却因为对性能问题的担忧而裹足不前的Java程序员,那么本书将引起你特别的兴趣。

Clojure 有助于重新定义,一种通用型编程语言应该包含哪些特性。如果你使用Lisp,或使用一种诸如Haskell这样的函数式语言,又或者正编写明显存在并发的程序,那你一定会享受Clojure的一切。Clojure融合了来自Lisp、函数式编程和并发编程领域的理念,使得初次接触这些概念的程序员,一切触手可及。

Clojure是本轮编程语言形态大规模演化现象的一部分。诸如Erlang、F#、Haskell和 Scala 这样的语言,由于它们支持函数式编程,或是由于它们的并发模型,最近都得到了格外的关注。作为这些语言的忠实信徒,你也一定会从Clojure当中找到众多共通之处。

本书主要内容

第1章,启航。本章将展示作为一门通用型语言,Clojure的优雅特质及其函数式风格,以及独特的并发模型如何令其独一无二。阅读完本章,你还将能够轻松完成Clojure的安装,并学会如何使用REPL进行交互式开发。

第2章,探索Clojure。在这里,我们将对Clojure的核心构造进行一次广度优先的概览。完成本章的阅读后,你将能顺畅地阅读大多数常规的Clojure代码。

接下来的两章将讨论函数式编程。第3章,一切皆序列,将展示Clojure如何使用强大的序列隐喻,统一了所有的数据形态。

第4章,函数式编程。本章将向你展示如何编写与序列库代码风格相同的函数式程序。

第5章,状态。本章我们将深入Clojure的并发编程模型。探讨Clojure中用于处理并发问题的 4 种强大模型。此外还有来自 Java 并发库中的精华内容一并奉上。

第6章,协议和数据类型。本章将逐个介绍在Clojure中的记录(record)、类型(type)和协议(protocol)。这些概念自Clojure 1.2.0版本首次引入后,在Clojure 1.3.0版本中得到了进一步增强。

第7章,宏。本章将不加掩饰地炫耀这一来自 Lisp 中的标志性特性——宏(Macros)。你将看到它如何利用“Clojure代码本身就是数据”这一特质,提供了在其他非Lisp语系中极难甚至无法实现的非凡的元编程能力。

第8章,多重方法。本章将讨论Clojure解决多态问题的众多方法中的一种。多态,通常意味着“获取第一个参数的类型,并据此调度到相应的方法”。Clojure 的多重方法,使你可以更进一步,选择适用于所有参数的任意函数来进行调度。

第9章,极尽Java之所能。在本章中,你将看到如何从Clojure中调用Java,以及从Java中调用Clojure。你还将看到如何让Clojure疯狂运转,获得原生Java级别的性能。

最后,第10章,搭建应用。本章提供了一个可以让你完整了解Clojure应用开发全过程的视角。在这里,你将从头开始创建一个应用,并深入解决问题的方方面面,同时,还会考虑关于简单和质量的话题。你将借助一组有用的Clojure库,生产并发布一个Web应用。

附录,编辑器。这里列出了可供你选择的Clojure代码编辑器列表,并分别提供链接指向它们各自的安装说明。

如何阅读本书

所有读者都应该按顺序阅读最初的两章。请特别关注1.1节,这里提供了Clojure具备哪些优势的概述,里面的内容你一定会感兴趣。

持续的试验。Clojure 提供了一个可以让你立即获取反馈的交互式环境。请阅读1.2.1小节,以获得更多的信息。

读完最初的两章,你就可以随意翻阅了。但如果你打算开始阅读第5章,那么,确保你已经读过了第3章。顺序阅读这几章,将引导你从理解Clojure的不可变数据结构开始,一直到能够利用Clojure强大的并发模型,编写正确的并发程序。

当你开始接触后面各章中那些较长的代码示例时,请确保你使用的编辑器能为你提供Clojure代码自动缩进功能。附录“编辑器”列举了编写Clojure代码的通常选择。如果可能,请尝试使用支持括号匹配功能的编辑器,例如Emacs的paredit模式或者安装了CounterClockWise插件的Eclipse。这些编辑功能将为你顺利学习Clojure编程提供巨大的帮助。

致函数式程序员

· Clojure的函数式编程之道,在于将理论的纯粹之美,与Clojure需要运行在当前Java虚拟机之上的现实做出了完美的平衡。倘若仔细地阅读了第4章“函数式编程”,你将了解到Clojure与诸如Haskell这样的学院派语言之间存在的风格差异。

· Clojure的并发模型(第 5章),提供了数个直截了当的途径,用于处理并发世界中副作用和状态的问题。这也使得广大读者可以深入地体验函数式编程之魅力。

致Java和C#程序员

· 请认真阅读第2章,Clojure只有很少的语法规则(相比Java或C#而言),所以我们能很快地熟悉它们。

· 请特别留意第7章,Java或C#背景的程序员将会发现,这部分是Clojure与他们所熟悉的语言之间的最大不同。

致Lisp程序员

· 第2章中的一些内容你可能已经很熟悉了,但无论如何,还是应该读一下这一章。Clojure从Lisp中承袭了众多关键特性,但它也在一些地方打破了Lisp传统,这里将会讨论这些内容。

· 请密切关注第4章中的惰性序列。

· 为你的Emacs装备一个“clojure-mode”吧,这将为你享受后面章节中的代码示例提供很大便利。

致Perl、Python和Ruby程序员

· 仔细阅读第5章,在Clojure中,进程内并行计算是一个非常重要的话题。

· 拥抱宏吧(第7章)。但请不要寄予太大的期望,能将你所用语言中的元编程风格轻松套用到 Clojure 宏中。请牢记,Clojure 的宏更为强大,并且,它是在代码读取期间被执行的,而非在运行期执行。

编写体例

以下编写体例将从始至终地贯穿于本书之中。

代码示例采用以下字体。

(+ 2 2)

为区别代码示例及其执行结果,我们会在执行结果前放置一个箭头(->)。

(+ 2 2)

-> 4

同样,控制台的输出也不容易与示例代码和结果区别开来,因此,我们会在控制台的输出前放置一个管道(|)符。

(println "hello")

| hello

-> nil

当首次引入某个Clojure形式(form),我们需要说明其语法时,将采用下述表示法。

(example-fn required-arg)

(example-fn optional-arg?)

(example-fn zero-or-more-arg*)

(example-fn one-or-more-arg+)

(example-fn & collection-of-variable-args)

这是一种非正式的语法,采用?、*、+和&符号,用于说明不同的参数传递模式。

Clojure的代码是以程序库的形式进行组织的。如果本书某段示例代码所依赖的库没有包含在Clojure语言核心中,我们将用Clojure的use或require对此加以说明。

(use '[lib-name :only (var-names+)])

(require '[lib-name :as alias])

此处使用use引入仅出现在列表var-names中的名称。使用require则创建一个库别名,使得每个引入函数的来源更加明晰。例如,来自于clojure.java.io库中的常用函数file。

(use '[clojure.java.io :only (file)])

(file "hello.txt")

-> #<File hello.txt>

或使用基于require的版本。

(require '[clojure.java.io :as io])

(io/file "hello.txt")

-> #<File hello.txt>

事实上,如果成功调用了use,Clojure会返回nil。但为使本书更加简洁,这个输出在示例清单中省略了。

在阅读本书期间,你将在名为REPL的Clojure交互式环境中输入代码。REPL的控制台提示符形如下。

user=>

提示符中的user表明了你当前所在的Clojure名字空间。在本书大多数的例子中,当前位于哪个名字空间无足轻重。在这种情况下,我们将其省略,采用下述更简洁的语法表示在REPL中发生的一切。

(+ 2 2); 没有命名空间提示的输入行

-> 4; 返回值

少数情况下,当位于哪个名字空间非常重要时,我们将采用如下语法。

user=> (+ 2 2); 有命名空间提示的输入行

-> 4; 返回值

Web资源及反馈

本书的英文官方主页位于Pragmatic Bookshelf站点。在这里你可以订购本书的纸质版或是电子版,并且下载本书的示例代码。同样,你也可以将你的反馈提交至勘误表或是直接发表至本书论坛

注释:① http://www.pragprog.com/titles/shcloj2/programming-clojure。

注释:② http://www.pragprog.com/titles/shcloj2/errata。

注释:③ http://forums.pragprog.com/forums/207。

下载示例源码

你可以在下列任意位置找到本书的示例源码。

本书主页上有链接指向官方发布的源码。同时,每次本书发布新版时,源码也将得到更新。

注释:④ http://www.pragprog.com/titles/shcloj2。

处于实时更新的本书git源码仓库。这里有最新、最棒,且有时甚至强过书中所示的源码。

注释:⑤ http://github.com/stuarthalloway/programming-clojure。

除非另行说明,示例文件都分别放在examples目录中。

贯穿于本书,示例源码的文件名列于源码清单起始位置,并采用灰色背景加以区别。例如,下面的源码清单来自于src/examples/preface.clj。

src/examples/preface.clj

(println "hello")

如果你正在阅读的是本书的PDF版本,你可以直接点击文件名下载对应的源码清单文件。

有示例源码在手,你就可以准备启航了。首先,我们将领略究竟是怎样的特性组合,使得Clojure如此的独一无二。

有许多因素共同促成了Clojure的快速崛起。不妨在网络上进行一次快速搜索,会看到人们是这样描述Clojure的。

· 是一种函数式语言。

· 是一种为Java虚拟机设计的Lisp。

· 具备处理并发问题的特殊能力。

所有这些特征都很重要,但却不是Clojure思想的核心。在我们看来,是这两个至为关键的概念驱动着Clojure的一切:简单、强大。

简单,在软件开发中被赋予了许多含义。但这里我们想要表达的,是它最初,也是最佳的定义:简单,就是不复杂。采用简单的组件,能让系统的设计者专注于他们手边的任务,从而免受各种无关杂事的打扰。以我们的经验来看,无关复杂性很快就会演变为危险复杂性。

强大,同样也具有很多含义。此处我们尤其关心,它是否足以完成我们将要承担的任务。作为一名程序员而言,强大,意味着你需要在一个功能丰富,且得到广泛部署的基础(例如Java虚拟机)之上构建应用。然后,你使用的工具必须能让你完全不受限制地访问这个平台。对于那些必须充分发挥平台能力的项目而言,这通常是决定性的需求。

作为程序员,我们年复一年地忍受着那些巴洛克式的复杂工具,仅仅因为这是我们获取必需的力量的唯一途径。如若不然,我们就不得不接受一个固然能使头脑保持清醒,但却弱小得多的简化编程模型。诚然,有一些取舍是无法回避的,但强大和简单绝不属于此列。Clojure向我们表明,强大与简单完全可以齐头并进。

所有Clojure的特色功能,要么简单,要么强大,或两者兼而有之。下面举几个例子。

· 函数式编程很简单,原因是它将计算的过程与状态及标识隔离开来。优点:函数式程序更容易理解、编写、测试、调优和并行化。

· Clojure与 Java的互操作极为强大,允许你直接访问 Java平台的语义。优点:你能拥有与Java等同的性能和语义。最重要的是,你不必为了获得这点额外的能力而“下降”到一门低级别的语言。

· Lisp的简单在于两个关键方面:它将代码的读取与求值分开了,并且语法仅由少数几个正交的部分构成。优点:能用语法抽象来捕获设计模式;此外,当需要的时候,S表达式(S-expressions)能成为XML、JSON或是SQL。

· Lisp也很强大,它在运行期提供了一个编译器和宏(macro)系统。优点:Lisp具有晚绑定的决策能力,并且很容易定制领域特定语言(DSL,Domain Specific Language)。

· Clojure的时间模型很简单,将值、标识、状态和时间相互分离。优点:程序可以放心地感知并记住信息,完全不必担心在这段时间里,有人正打算对其乱涂乱画一番。

· 协议(Protocols)很简单,将多态性(polymorphism)和派生(derivation)分离。优点:不必纠结于设计模式,或是依赖于脆弱的猴子补丁(monkey patching),你就能得到既安全又极富扩展性的类型与抽象。

这个功能列表可以作为本书剩余部分的路线图,所以,即便此刻你尚无法充分理解每个小细节,也不必太过忧虑。上面的每个特性,都分别用了整整一章来加以详述。

让我们构建一个小型的应用,看看其中一些特性是如何运作的。沿途你将学会如何加载并执行那些较大的示例,本书的后半部分会用到它们。

Clojure高信号,低噪音。因此,Clojure程序都非常简短。短小的程序,无论是构建、部署,还是维护,都要便宜得多。尤其当程序是简明的(concise)而不仅仅是简短(terse)的时候就更是如此了。举个例子,考虑下面这段来自于 Apache Commons的Java代码。

注释:①《软件软算:黑黑子黑黑》 [McC06]这是一部重要的著作,里面有越小越便宜的实例。

data/snippets/isBlank.java

public class StringUtils {

 public static boolean isBlank(String str) {

  int strLen;

  if (str == null || (strLen = str.length()) == 0) {

   return true;

  }

  for (int i = 0; i < strLen; i++) {

   if ((Character.isWhitespace(str.charAt(i)) == false)) {

    return false;

   }

  }

  return true;

 }

}

isBlank()方法用于检查目标字符串是否是空白的:没有任何字符,或者只包含空格符。这里是Clojure的类似实现。

src/examples/introduction.clj

(defn blank? [str]

  (every? #(Character/isWhitespace %) str))

Clojure版本要短得多。但更重要的是,它更加简单:没有变量,没有可变状态,也没有分支结构。这可能要归功于高阶函数(higherorder functions)。高阶函数本身也是一个函数,它接受其他函数作为参数;也可以把函数作为返回值。every?函数接受一个函数f和一个容器(collection)c作为它的参数,对于容器c中的每个元素,如果函数f都返回真的话,every?函数也就返回真。

注释:① 译注:本书中将collection译作容器而不是集合,是为了与set类型加以区分。它们与J2EE中的EJB容器或Servlet容器没有任何关系。

由于Clojure的这个版本没有分支结构,所以无论是阅读还是测试都更容易。在大一些的程序中,这种优势还将会进一步扩大。而且,简洁的代码也更具可读性。事实上,Clojure的这段程序读起来就像是一份关于何为空白的定义:如果一个字符串中的每个字符都是空格,那么这个字符串就是空白的。这要比一般的方法好太多了,在那些方法中,对空白的定义被隐藏在了由循环和分支语句组成的实现细节背后。

另外一个例子,考虑用Java定义一个微不足道的Person类。

data/snippets/Person.java

public class Person {

 private String firstName;

  private String lastName;

 public Person(String firstName, String lastName) {

   this.firstName = firstName;

   this.lastName = lastName;

 }

 public String getFirstName() {

  return firstName;

 }

  public void setFirstName(String firstName) {

   this.firstName = firstName;

 }

 public String getLastName() {

  return lastName;

 }

 public void setLastName(String lastName) {

   this.lastName = lastName;

 }

}

在Clojure中,用一行代码就可以定义这个Person。

(defrecord Person [first-name last-name])

然后像下面这样使用:

(def foo (->Person "Aaron" "Bedra"))

-> #’user/foo

foo

-> #:user.Person{:first-name "Aaron", :last-name "Bedra"}

在第6.3节“协议”中,包含了defrecord及其相关函数的介绍。

除代码短了一个数量级以外,Clojure 采用的方法还有一处不同:Clojure 版本的 Person 是不可变的。不可变数据结构生来就是线程安全的,Clojure 中可以通过使用引用、代理和原子来更新数据,这些内容将在第5章“状态”中详加讨论。正因为记录(record)是不可变的,Clojure也就自动提供了正确的hashCode()和equals()实现。

Clojure内建了大量优雅的特性,但倘若你发现还是遗漏了某样东西的话,你可以自己添上,这完全要归功于Lisp的强大。

Clojure是一种Lisp方言。数十年来,拥护者们指出了Lisp与其他语言相比的诸多优点。但同时,Lisp一统天下的计划看起来却遥遥无期。

如同其他所有的Lisp一样,Clojure也面临着两个挑战。

· Clojure必须成功地说服Lisp程序员,作为一种Lisp方言,Clojure包含了Lisp的关键部分。

· 同时,Clojure 还需要成功地赢得广泛的程序员社区支持,而这正是过去那些Lisp的失败之处。

为了应对这些挑战,Clojure提供了Lisp元编程能力,与此同时还包含了一整套增强的语法,使得Clojure对于非Lisp程序员而言显得更为友好。

Lisp的语言核心非常小,几乎没有什么语法,但却提供了一个强大的宏设施。借助这些特性,你可以根据你的设计需要对Lisp随意地直接定制。这样就不必使用其他那些绕来绕去的方式了。考虑以下Java代码片段。

public class Person {

  private String firstName;

  public String getFirstName() {

 // 以下省略…

在这段代码中,getFirstName()是一个方法(method)。方法具有多态性,可以根据你的需要加以调整。但对于Java而言,示例中其他单词的语义,其解释都是固定的。然而,有时你确实需要改变这些词语的含义。举例来说,你可能会像下面这么做。

· 重新定义private:对于产品代码保持私有,但允许来自序列化(serialization)和单元测试代码的访问。

· 重新定义class:自动为每个私有字段都生成getters和setters,除非另有指示。

· 创建class的一个子类,提供面向生命周期事件的回调钩子。例如,对于“可感知生命周期”的类而言,只要创建了这个类的一个实例,就会激发相应的事件。

我们一定见过需要上述特性的程序。由于缺乏这些特性,程序员们不得不去借助一些重复性的、容易出错的变通方法。结果是,人们在这上面白白浪费了数百万行的代码,而罪魁祸首就是编程语言中类似特性的缺失。

对大多数编程语言而言,你只能祈求语言的实现者们尽快增加上面提到的这类特性。但在Clojure中,你能凭借宏来自行添加属于你自己的语言特性(第7章“宏”)。事实上,Clojure本身就是用宏来进行扩建的,比如defrecord。

(defrecord name [arg1 arg2 arg3])

如果你需要的语义与此不同,写一个你自己的宏就行。比如你想得到记录的一个变种,它具备强类型并具有可选的空字段校验能力,你可以创建自己的defrecord宏。这个新的defrecord用法如下。

(defrecord name [Type :arg1 Type :arg2 Type :arg3]

  :allow-nulls false)

这种对语言进行再编程,从而改变语言自身的能力,是Lisp的独门优势。下面用不同的方式来描述这一思想。

· Lisp具有同像性(homoiconic)。也就是说,Lisp代码其实就是Lisp数据。这样就很容易让程序自己去编写其他的程序。

注释:① http://en.wikipedia.org/wiki/Homoiconicity。

· 这就是语言的全部,且始终如此。保罗·格雷厄姆在其散文《书呆子的复仇》中,解释了为什么这会如此的强大。

注释:② http://www.paulgraham.com/icad.html。

Lisp语法也废除了运算符优先级和结合性的规则。翻遍本书的任何一个角落,你都不会看到用来说明运算符优先级或结合性的表格。凭借完全的括号表示法,就能避免产生任何这方面的歧义。

简单、整齐的Lisp语法也存在负面因素,至少对于初学者而言,成堆的括号,以及将列表作为核心数据类型都会成为一种障碍。为此,Clojure提供了有趣的功能组合,对于非Lisp程序员而言,这个Lisp显得要亲切得多。

对于来自其他Lisp方言的程序员来说,Clojure的优势显而易见。

· Clojure泛化了Lisp的物理列表,将其抽象为序列(sequence)。这样既保留了列表的强大能力,同时还将这种能力扩展到了其他各种类型的数据结构。

· 依托于Java虚拟机,Clojure提供了一个范围广泛的标准库及部署平台。

· Clojure提供的符号解析和语法引述(syntax quoting)方式,使得编写许多普通宏的时候更加容易了。

许多Clojure程序员可能会是Lisp的新手,他们也许听说过诸多关于Lisp括号的可怕传言。是的,Clojure保留了括号表示法(当然也保留了Lisp的强大!),但在以下方面对传统Lisp语法进行了改进。

· 在Clojure中,除列表之外,还提供了更为便利的正则表达式、映射表、集合,向量和元数据等多种数据结构的字面表示语法。这些特性使得Clojure代码相比其他多数Lisp语言而言,过度列表化(listy)的症状要轻很多。例如,Clojure函数的参数是在一个向量([])中指定的,而不是使用列表(())。

src/examples/introduction.clj

(defn hello-world [username]

  (println (format "Hello, %s" username)))

向量令参数列表变得非常醒目,也使得Clojure的函数定义更易于阅读。

· 与大多数Lisp语言不同,在Clojure中,逗号就是空格。

; 这让向量看起来就像是其他语言中的数组一样。

[1, 2, 3, 4]

-> [1 2 3 4]

· 地道的Clojure不会内联不必要括号。考虑一下在Common Lisp和Clojure中都有的cond宏。cond对一组成对的“测试/结果”逐个求值,当遇到第一个求值结果为真的测试时,返回其对应的结果。Common Lisp 中,每一对“测试/结果”都得像下面这样,用括号进行分组。

; Common Lisp cond

(cond ((= x 10) "equal")

  ((> x 10) "more"))

而在Clojure中则避免了额外的括号。

; Clojure cond

(cond (= x 10) "equal"

  (> x 10) "more")

这是一种审美决定,且双方都各有其支持者。但重点在于,Clojure获得了在不减损Lisp威力的前提下,尽可能减少过度列表化的机会。

Clojure是一种卓越的Lisp方言,无论对于Lisp专家,还是Lisp新手,皆是如此。

Clojure虽然是一种函数式语言,但不像Haskell那样纯粹。函数式编程语言具有下列属性。

· 函数是一等公民。换言之,函数能在运行期间被创建,被当做参数传递,被用作返回值,并且能像其他数据类型那样,被用于各种用途。

· 数据是不可变的。

· 函数是纯粹的,也就是说,它们不会造成任何副作用。

对许多任务而言,函数式程序更容易理解,不容易出错,且更利于重用。例如,下面这个小程序从乐曲数据库中,查询有哪些作曲家创作了《Requiem(安魂曲)》。

(for [c compositions :when (= "Requiem" (:name c))] (:composer c))

-> ("W. A. Mozart" "Giuseppe Verdi")

这里的 for ,并不意味着引入了循环,而是进行了一次列表解析( list comprehension)。所以,这段代码应该这么读:“对于乐曲库中的每支乐曲 c,当 c 的名称是《Requiem》时,则获取c的作曲家信息”。本书第3.2.4小节“序列转换”中有关于列表解析的完整讨论。

这个例子的可取之处有以下4方面:

· 非常简单,没有任何循环结构、变量或是可变的状态;

· 线程安全,不需要锁机制即可得到保证;

· 可并行化,无需修改代码,你就可以将单独的步骤转移至多个线程;

· 非常通用,乐曲库可以是一个普通集合、XML或是一个数据库结果集。

这里,函数式程序与命令式程序形成鲜明对比,在命令式程序中,是用显式的语句来改变程序状态的。大多数面向对象程序都是采用命令式风格写就的,在前面列出的这几方面,它们劣势尽显(关于函数式和命令式风格的逐项对比,请阅读2.7节)。

如今人们已经知道了函数式语言的优势。然而,像Haskell那样的纯函数式语言却没能接管世界,这是因为开发者们发现,纯粹的函数式观点无法轻易地解决所有问题。

与过去的那些函数式语言相比,有4个原因使得Clojure能够吸引更多的注意。

· 对函数式编程的需要,比以往任何时候都显得更加迫切。规模庞大的多核硬件已指日可待,函数式语言提供了一种清晰的方式对其加以利用。本书第4章“函数式编程”详细讨论了这个话题。

· 当确实需要对状态进行修改时,纯粹的函数式编程语言就显得颇为尴尬了。Clojure则通过软事务内存(STM,software transactional memory)及引用、代理、原子和动态绑定,提供了结构良好的机制用于处理可变状态。

· 许多函数式语言都是基于静态类型的。而Clojure的动态类型系统,使得程序员学习函数式编程更加容易。

· Clojure的 Java调用方式是非函数式的。当你调用 Java程序时,你会进入那个熟悉的,可变的世界。这为函数式编程的初学者提供了一个舒适的港湾,此外当你需要时,这也是能够提供函数式风格替代品的务实之选。第9章“极尽Java之所能”详细讨论了关于Java调用方面的内容。

Clojure 中不必显式锁定,就允许并发地更改状态。这种方式是 Clojure 函数式核心的有力补充。

Clojure支持函数式编程,使得编写线程安全的代码非常容易。由于不可变数据结构在任何时候都不会被修改,因此避免了数据会被另外一个线程破坏的危险。

然而,仅仅是函数式编程,还不足以体现Clojure对并发程序支持之卓越。当你需要引用可变数据时,Clojure会通过软事务内存对其加以保护。在线程安全方面,相比Java提供的锁定机制,软事务内存是一种更高级的方法。你可以借助事务来保护共享状态,而不是去琢磨那些既脆弱,又易于出错的锁定策略。源于数据库方面的经验,很多程序员对何为事务早就了然于胸,所以这也是一种更富成效的做法。

例如,下面的代码创建了一个线程安全的内存数据库用于存放账号。

(def accounts (ref #{}))

(defrecord Account [id balance])

ref函数创建了一个引用,代表数据库的当前状态,这个引用会得到事务的保护。更新操作实在是微不足道。下列代码向数据库中添加一个新的账号。

(dosync

  (alter accounts conj (->Account "CLJ" 1000.00)))

dosync 开启了一个事务,允许对accounts 进行更新。这样既确保了线程安全,同时也比锁机制更容易使用。得益于事务,你不必再操心应该锁定哪些对象,以及应该以什么顺序来锁定等等问题。在一些常见的使用场景中,因为读取操作不会被阻塞,所以事务机制能够非常高效地运转。

虽然这是个微不足道的例子,但其展现的技术是通用的,完全可用于解决现实世界中的问题。请参阅第5章“状态”,那里有更多关于Clojure中并发及软事务内存方面的讨论。

从Clojure访问Java,清晰、简单、直接。你能直接调用任何JavaAPI。

(System/getProperties)

-> {java.runtime.name=Java(TM) SE Runtime Environment

... many more ...

Clojure为调用Java提供了很多语法糖。我们不需要在这里深入过多细节(参阅第2.5节“调用Java”),但请注意,下面的代码中,Clojure的那个版本无论是点号(.),还是括号(()),数量都比Java版本要少。

// Java

"hello".getClass().getProtectionDomain()

; Clojure

(.. "hello" getClass getProtectionDomain)

Clojure提供了简单的函数用于实现Java接口,以及从Java基类派生。此外,Clojure的所有函数都实现了Callable和Runnable接口。这使得采用下面所示的匿名函数来构建Java线程竟然如此轻松。

(.start (new Thread (fn [] (println "Hello" (Thread/currentThread)))))

-> Hello #<Thread Thread[Thread-0,5,main]>

这里有个有趣之处,就是Clojure打印Java对象实例的方式。Thread是这个实例的类名,然后Thread[Thread-0,5,main]是这个实例的toString方法返回值。

注意,前例中的这个新线程会持续运行直至完成,但其输出可能会以某种奇怪的方式,同REPL的提示符产生交错现象。但这并非Clojure的问题,只不过是有多个线程同时向输出流进行写入数据的结果罢了。

由于在Clojure中调用Java程序的语法干净而且简单,作为Clojure的惯例,会更加倾向于直接对Java进行调用,而不是把Java隐藏到一层Lisp化的封装背后。

好了,现在你已经看到一些为什么要使用Clojure了,是时候开始编写一些代码了。

要运行Clojure及本书的示例代码,你需要两件东西。

· Java运行时。请下载并安装 Java 5或是更高版本。Java 6具有显著的性能提升和更好的异常报告,如果可能就尽量选它吧。

注释:① http://www.oracle.com/technetwork/java/javase/downloads/index.html。

· Leiningen。Leiningen是一个用于管理依赖项的工具,并且可以基于你的代码启动各种任务。在Clojure世界中,它是处理这项工作最常用的工具了。

注释:② http://github.com/technomancy/leiningen。

你将会使用Leiningen来安装Clojure和本书所有示例代码的依赖项。如果你已经安装了Leiningen,那就应该已经熟悉相关的基础知识了。否则,你应该快速的浏览一下Leiningen的GitHub主页,在那儿你能找到如何安装,以及其基本用法的说明。现在还不用急着去学习所有这一切,因为本书会指引你轻松地掌握那些必须的命令。

注释:③ http://github.com/technomancy/leiningen。

在阅读本书期间,请使用与本书示例代码匹配的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.”做好了准备。

来看看如何使用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),然后再另外启动一个。

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函数(print stacktrace)得到堆栈跟踪信息。

注释:① pst函数仅适用于Clojure1.3.0及更高版本。

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。

上一节中的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库。下面让我们来探索一下吧。

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命令来运行这些单元测试。

当你用require加载了一个Clojure程序库,就需要使用命名空间限定的名称来引用这个库里面的内容。你必须这么写:examples.introduction/fibs,而不是简单地写下fibs了事。下面,确保你新启动了一个REPL,然后试着输入。

注释:① 新开启一个 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标记就能帮上大忙。

通常情况下,你都可以在REPL中找到你需要的文档。最基本的辅助函数是doc。

注释:① doc实际上是一个Clojure的宏。

(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的特性的文章。

你刚才完成了一次旋风般的Clojure之旅。你已经看到了Clojure极富表现力的语法,了解了Clojure的Lisp之道,还见到了从Clojure中调用Java代码有多么容易。

你已经有了一份运行在你自己环境中的Clojure,此外你还在REPL中编写了几款短小的程序,用来演示函数式编程和解决状态问题的引用模型。现在是时候去探索一下整个语言了。

相关图书

Clojure Web开发实战
Clojure Web开发实战
深入理解Scala
深入理解Scala
Haskell并行与并发编程
Haskell并行与并发编程
Haskell函数式编程入门
Haskell函数式编程入门
Haskell趣学指南
Haskell趣学指南
Clojure编程乐趣
Clojure编程乐趣

相关文章

相关课程