Clojure编程乐趣

978-7-115-31949-4
作者: 【美】Michael Fogus Chris Houser
译者: 郑晔
编辑: 陈冀康

图书目录:

详情

本书介绍的内容超出了语法,不只是如何编写Clojure代码;还将介绍函数式编程方法,并且将帮助读者掌握Lisp技术,以使得Clojure更为优雅和高效。本书侧重介绍如何以Clojure的方式思考问题。

图书摘要

Clojure编程乐趣
〔美〕Michael Fogus Chris Houser 著

郑晔 译

人民邮电出版社

北京

内容提要

Clojure是一门Lisp方言。它通过函数式编程技术,直接支持并发软件开发,得到众多开发人员的欢迎。

本书并非Clojure初学指南,也不是一本Clojure的编程操作手册,而是通过对Clojure详尽地探究,教授函数式的程序设计方式,帮助读者理解和体会 Clojure 编程的乐趣,进而开发出优美的软件。

全书分为5个部分共13章。第1部分是基础,包括第1章到第3章,从Clojure背后的思想开始,介绍了Clojure的基础知识,并带领读者初步尝试Clojure编程。第2部分包括第4章和第5章,介绍了Clojure的各种数据类型。第3部分是第6章和第7章,介绍了函数式编程的特性。第4部分包括第8章到第11章,分别介绍了宏、组合数据域代码、Clojure对Java的调用,以及并发编程等较为高级的话题。第5部分为第12章和第13章,探讨了Clojure的性能问题及其带给我们的思考。

本书适合有一定基础的 Clojure 程序员阅读,进而掌握函数编程的思考方法和程序设计方法,也可以作为读者学习函数式编程的参考资料。

本书作者选择了一种极具野心且颇为迚取的方式教授Clojure。当听闻有人迚行“疾风式”教学,你会做何感想?喔,感觉就像有人马上就要被吹走一样……我只是说,这不是通常理解的疾风。本书根本没打算成为程序设计的第一本书,即便是第一本Clojure书也不合适。作者假设你是个无畏的家伙,重要的是,你还配备了搜索引擎。浏览书中例子时,手边最好有Google。在这场Clojure旋风之旅中,作者带着我们飞快地领略了函数式编程和工业程序设计的经典基础,偶尔会让人觉得这简直是场五级热带风暴。你会学得飞快!

我们的产业,甚至整个的程序设计社区,都是时尚驱动的,以至于从纽约到巴黎高级服装设计师都局促不安。我们臣服于时尚。时尚决定着学校里教授怎样的程序设计语言,语言雇主招什么样的人,书架上摆什么书。天真的局外人或许以为语言的质量多少会有点影响,至少有那么一点点,但在现实世界里,时尚压倒一切。

所以,突然有一门Lisp方言流行起来,没有人会比我更为惊讶了。Clojure仅仅面世三年 [1],却以数十年间前所未见的速度赢得关注。它甚至还没来得及有个“杀手级应用”,就像浏览器将JavaScript推到了闪光灯下,Rails促迚了Ruby那样。或者说,也许Clojure的杀手级应用就是JVM本身。所有人对Java语言都忍无可忍,但有一点却可以理解,我们幵不打算放弃在Java虚拝机及其能力上的投资:程序库、配置、监控,以及所有各种完全有效的理由,都支持我们继续用下去。

对于使用JVM或是.NET的我们而言,Clojure感觉就像一个小奇迹。它的确是一门不可思议的高质量语言,实际上,我已经开始认为它是我见过的最好的程序设计语言了——然而不知怎么它就流行起来了。这简直是个魔法!它重燃了我对这个行业未来生产力整体提升的希望。或许,我们只是想摆脱困境,回到每个项目都像全新启动一样,没有遗留系统,如同Java的往日荣光一般。

在Clojure对生产环境的支持上,还有许多问题悬而未决,特别是相关的工具链。对于一门新语言,这是很正常的,也在预期之中。但是,Clojrue让我们看到了希望,如此优美实用的设计原则,似乎每个人都会为之雀跃。我的确如此!自从15年前Java降临,我已许久未曾体会到新语言带来的乐趣了。有许多语言觊觎 JVM 的王座,承诺将 Java平台带至前所未有的新境界。时至今日,没有一种语言能将表达性、工业强度、性能同简单的乐趣正确地融合在一起。

在我看来,也许正是Clojure中“乐趣”的部分使之流行起来。

从某种意义上说,我认为所有这些都无可避免。Lisp——直接以树形式编写代码的记法——这种理念已经是一次又一次得到了时间的验证。人们尝试过各种疯狂的做法:用XML格式,不透明的二迚制,甚至用笨拙的代码生成器编写代码。但这种人造的“拜占庨帝国”总会年久失修,或为自身所累而坍塌崩溃,然而Lisp却历经岁月,依然简单、优雅、纯净。我们需要以一种现代的方式回到这条路上来。Rich Hickey 做到了,他用Clojure带我们回来了。

本书或许只是让Clojure有趣起来,对您如此,对我们也如此!

STEVE YEGGE

GOOGLE

steve-yegge.blogspot.com

《A Programmer’s Rantings》作者

注释

[1]. 译注:Clojure 诞生于2008 年,而本书英文版出版于2011 年。

前言

想要完全理解 Clojure,就应该品味一番 Paul Graham 的文章《拒绝平庸》,它让人有机会一窥其公司 Viaweb 在 1998 年被雅虎收购之前的内部状态。虽然纵览创业文化很有趣,但这篇文章真正令人难以忘怀的部分是,Viaweb 怎样用 Lisp 程序设计语言赢得竞争优势。一门五十多岁的程序设计语言怎么能为 Viaweb 带来超越其竞争对手的优势呢?它的竞争对手肯定用的是更现代的企业级技术。这里无意重复文章中的确切内容, Graham 确实给出了一个令人信服的例子,充分展现了 Lisp 在促迚更加敏捷程序设计环境方面的能力。

Clojure 是一门 Lisp 方言,通过函数式编程技术,直接支持幵发软件开发。它类似于《拒绝平庸》中所描述的Lisp,提供了一个有益于敏捷性的环境。Clojure 以一种许多流行的程序设计语言无法复制的方式促迚了敏捷性的发展。许多程序设计语言受困于下列事情的全部或大多数:

● 啰嗦。

● 无可避免的样板代码(boilerplate)。

● 漫长的思考-编码-反馈循环。

● 偶然复杂性。

● 难于扩展。

● 缺乏对关键程序设计范式的支持。

相比之下,Clojure 将能力和实践融合在一起,缩短了开发周期。但是,Clojure 的益处幵没有止步于其敏捷性——正如一篇文章所强烈地召唤一般,《多核已成新热点(Multicore is the new hot topic)》(Mache Creeger in ACM Queue, vol. 3, no. 7)。

虽然多核处理器概念本身幵不新,但其重要性正日益赢得更多的关注。时至今日,幵发和幵行程序设计已然无法回避,而凭借不断加快的处理器浪潮赢得更好性能的往日荣光已经一去不回。好吧,它正逐步放缓,直至停止,Clojure恰逢其时的出现,给了我们很大的帮助。

Clojure将函数式编程与宿主共生以一种独特的方式融合在一起,这种宿主共生是对宿主平台的拥抱和直接的支持,这里宿主平台指的是 Java 虚拟机。此外,Clojure 简化有时甚至消除了需要协调的状态改变所包含的复杂性,这也将 Clojure 定位成一种重要的、勇往直前的语言。最终,所有的软件开发人员都会将处理这些问题视为理所当然,对于 Clojure 的研习、理解以及最终的运用都是征服这些问题的必经之路。从软件事务性内存,到惰性,再到不变性,本书将引导你理解Clojure这些主题背后的“为什么”,当然,还包括了“怎么做”。

我们愿做你的向导,帮你深入理解 Clojure 的乐趣,因为我们相信,这种艺术终将成为新时代软件开发的序曲。

致谢

本书作者要共同感谢 Rich Hickey,Clojure 之父,带给世人其深思熟虑之作,推动了语言设计迚一步发展。没有他的辛勤工作、投入及视野,本书便不复存在。

我们还要感谢年轻的 Clojure 社区里那些充满智慧的人,包括但不限于 Stuart Halloway、David Edgar Liebke、Christophe Grand、Chas Emerick、Meikel Brandmeyer、Brian Carper、Bradford Cross、Sean Devlin、Tom Faulhaber、Stephen Gilardi、Phil Hagelberg、Konrad Hinsen、George Jahad、David Miller、David Nolen、Laurent Petit 和Stuart Sierra。之后,我们要感谢一些早期阅读者,他们经过深思熟虑,给出了周密的反馈,包括 Jürgen Hötzel、Robert “Uncle Bob” Martin、Grant Michaels、Mangala Sadhu Sangeet Singh Khalsa和Sam Aaron。最后,我们要特别感谢Steve Yegge 同意为本书写序,许多年来,他一直激励着我们。

在本书编写的不同阶段,Manning都会把草稿发出去做评审,我们感谢下列评审者的无价建议:Art Gittleman、Stuart Caborn、Jeff Sapp、Josh Heyer、Dave Pawson、Andrew Oswald、Federico Tomassetti、Matt Revelle、Rob Friesel、David Liebke、Pratik Patel、Phil Hagelberg、Rich Hickey、Andy Dingley、Baishampayan Ghose、Chas Emerick、John D'Emic和Philipp K. Janert。

还要感谢Manning团队给予的指导和支持,从发行人Marjan Bace 开始、副发行人Michael Stephens、我们的开发编辑Susan Harkins 以及生产团队Nicholas Chase、Benjamin Berg、Katie Tennant、Dottie Marsico 和Mary Piergies。还要再次感谢Christophe Grand,在生产期间给予脚本最终的技术审校。

FOGUS

我要感谢我美丽的太太Yuki,在写作本书期间,她给予了我无限的耐心。没有她,我恐怕无法坚持下来。我还亏欠Chris Houser 许多,我的合作者和朋友,他教会了我许多Clojure 的东西,我从未想到可以这么用。我要感谢Larry Albright 博士,他把Lisp 介绍给我,稍后,Russel E. Kacher 博士,他激发了我对学习、好奇心以及沉思的激情。此外,我要感谢National Capital Area Clojure Users Group 的组织者——Matthew Courtney、Russ Olsen和Gray Herter,他们提供了一个地方给DC 地区的其他人讨论Clojure。最后,我要感谢我的儿子Keita和Shota,他们教会我爱的真正含义,并不总是为了我个人。

CHOUSER

感谢我的父母,感谢你们的爱与支持——你们的探索精神开启了我的奇妙冒险之旅。我的哥哥Bill,感谢你最早把我带入计算机的世界,领悟程序设计的乐趣与挑战。我的爱人 Heather,你始终如一的鼓励,贯穿了本书从始至终的创作历程。我的朋友和合作者Michael Fogus,感谢你绝妙的灵感以及令人叹为观止的知识宽度,才有诸位手上的这本书。

关于本书

为何学习Clojure?

你与莎士比亚之间仅有的差别在于习语的数量——而非词汇量的多寡。

—Alan Perlis

本书酝酿之际,我们的第一直觉是将Clojure与其宿主语言Java做一个全方位的比较。经过深入反思,我们得到的结论是,做好了最多算是狡猾,搞不好则是灾难。诚然,一些比较无可避免,但Java与Clojure有着很大的不同,为了说明一个而试图歪曲另一个,对二者都有失公允。因此,我们决定采用一种更好的方式,专注于编写代码本身的“Clojure之道 ”。

当我们熟悉了一门程序设计语言,这门语言的惯用法和构造就会定义我们思考以及解决程序设计任务的方式。因此,面对一门全新的语言时,我们很自然地就会在精神上将新语言映射为我们熟悉的旧语言。但是,我们恳求你,请将所有的包袱丢在身后;无论你来自Java、Lisp、Scheme、C#或是Befunge,我们都请你铭记,Clojure有其自身的语言,请遵循它自己的一套惯用法。你会发现,Clojure与你已然熟知的语言之间在概念上有着一些联系,但请千万不要假设类似的东西就是完全一样的。

我们会努力工作,引导你了解 Clojure 用于构建思维模型的特性和语义,这样才能更有效地使用语言。本书的大多数例子都设计成可以在 Clojure 的交互式程序设计环境中运行,通常称为读取——求值——打印循环(Read-Eval-Print Loop),或是 REPL,这是一个极其强大的环境,用于实验和做快速原型。

当你读完本书,用 Clojure 之道思考以及解决问题将成为你的另一片舒适区。如果我们成功了,那么,你不仅可以成为一个更好的 Clojure 程序员,还能够以别样的视角看待程序设计语言的选择——无论是Java、C#、Python、Ruby还是Haskell。重新评估一些我们已经认为是理所当然的主题,对于个人成长而言,是不可或缺的。

谁该阅读本书?

路是走出来的。

—Franz Kafka

本书并非Clojure初学指南。虽然我们的确提供了一些入门指导,但我们起步极快,没有在搭建可运行的Clojure环境上花费太多精力。此外,本书讨论的并非实现细节,而是语义细节。这也不是一本Clojure的“cookbook”,取而代之的是,通过对Clojure详尽的探究,为创建优美的软件提供素材。我们常常会解释这些素材如何整合,为什么它们搭配极佳,但这里没有全面的系统秘籍。我们的例子直接处理了手头的一些东西,有时还会把一些东西留给你去扩展,进一步丰富自己的知识。别指望我们将一门全面的课程装到一本书里,无论对你我,还是对 Clojure,这都是不可能的。通常,一本语言的书要花掉一半的篇幅介绍“真实世界”的情况,这与语言本身完全没有关系,希望我们能够规避这个陷阱。我们有一种强烈的感觉,如果能够告诉你语言背后的“为什么”,那你就能做好准备,将这些知识应用于真实世界的问题。简而言之,如果你找的是一本对新手负责的书,告诉你如何迁移既有代码库,连接NoSQL数据库以及探索其他“真实世界”的主题,那我们推荐Amit Rathore的《Clojure in Action》(Manning, 2011)。

总而言之,我们确实提供了一份语言简介,我们认为,如果你是愿意花时间理解Clojure 的人,这本书就是为你准备的。此外,如果已经有了 Lisp 程序设计的背景,那你会觉得许多介绍材料看起来都很熟悉,所以,这本书对你而言是理想的。虽然绝非完美,但对于解决实际的程序设计问题,Clojure拥有一套很好的特性组合,能够放入一致的系统中,解决程序设计中的问题。Clojure鼓励我们思考问题的方式可能不同于我们习惯的方式,需要花些精力才能“得到”。但是,一旦跨过了这个门槛,我们或许能体会到一种愉悦,在本书里,我们就是想帮你到达那里。这是令人兴奋的时刻,我们希望你会同意,Clojure会是带我们驶入未来所不可或缺的一种工具。

路线图

我们要带你上路了。也许之前你已然开启了自己的Clojure探索之旅。也许你是个Java或Lisp老手,第一次接触Clojure。也许你来自完全不同的背景。无论如何,我们在对你说。本书自诩为写给冒险者,它需要我们丢开自己的包袱,带着开放的心态了解其中的主题。在很多方面,Clojure 会改变我们看待程序设计的方式,在其他一些方面,它会冲刷掉我们预先形成的一些理念。关于软件如何设计与实现,这门语言有很多要说的,本书将逐一触及这些主题。

根基

几乎每一种程序设计语言都有一些被认为是根本性的东西。偶尔,一种语言发明出来会动摇软件产业的根基,驱散一些广为人知的既有的关于“良好软件实践”的概念。那些根本性的语言总能将一些全新的方式引入软件开发,缓和那个时代一些困难的问题,如果不能完全消除的话。任何一份根本性语言的列表都无可避免的会引发某些语言支持者的愤慨,在他们看来,其喜好的语言不应被忽略。但是,我们愿意承担此风险,所以,仅将下列程序设计语言归为此类。

根本性的程序设计语言

无论喜欢与否,少有争论的一点是,所列语言极大地影响了软件构造的方式。Clojure是否应位列其中尚待观察,但是,Clojure确实从诸多根本性的语言中借鉴了许多,其他有影响力的程序设计语言也让它获益良多。

第1章开启了我们的旅程,介绍了Clojure蕴含的一些核心概念。此章完结之时,我们应该可以很好地理解这些概念。沿途之中,我们展示了一些说明性的代码样例,突显了一些概念(还有些不错的图)。第1章所包含的多数内容均可以视为“Clojure哲学”,因此,如果你想知道Clojure受什么启发以及由什么组成,这章就是为你准备的。

第2章快速地介绍了Clojure特定的特性和语法。

第3章讨论了一些不易归类的通用Clojure程序设计的惯用法。从真值和风格,到打包和nil的考量,第3章就是个大杂烩。所有的主题本身都很重要,从许多方面来看,这些内容是理解大部分Clojure惯用源码的起点。

数据类型

第4章讨论了标量数据类型,大多数程序员对这个话题相对熟悉,但还有一些重点需要注意,源于Clojure一些有趣的特性,这是寄宿于Java虚拟机的函数式程序设计语言所固有的。阅读本书的Java程序员会关注数学精度(4.1节),Lisp程序员则会关注Lisp-1 vs.Lisp-2(4.4 节)。Clojure 里还包含了实用的正则表达式,并将其作为一等语法元素(4.5节),程序员们会对此心存感激的。最后,长期的Clojure程序员也许会发现,关于有理数和关键字(分别在4.2和4.3节)的讨论,对这些貌似无辜的类型给出了全新的见解。

无论背景如何,第4章都会提供一些关键信息,帮助我们理解 Clojure 那些未受重视的标量类型的本性。

第5章涵盖了Clojure全新的持久化数据结构;任何希望深入了解它们的人都能够从中获得启迪。持久化数据结构位于Clojure程序设计哲学的核心,必须理解方能完全掌握Clojure设计决策的含义。我们只会简要触及这些持久化结构的实现细节,因为相对而言,理解为什么使用以及如何使用这些结构会更重要一些。

函数式编程

第6章会让人们对不变性、持久化和惰性有个大致的了解。我们会探索Clojure在支持并发程序设计中的关键元素:不变性。类似地,我们还会看到,有了不变性,许多与需要协调状态改变的问题都消失殆尽了。我们还会探索Clojure利用惰性降低内存占用以及加速执行时间的方式。最后,我们会谈及不变性和惰性的相互作用。如果你来自那些对修改不加限制且拥有严格求值表达式的语言,初涉之下,第6章或许是一种令人费解的体验。但这种令人费解会带来某种启迪,我们可能以前所未有的视角审视我们最喜欢的程序设计语言。

第7章全面展示了Clojure式的函数式编程。如果有函数式编程的背景,那你会熟悉本章的很多内容,虽然 Clojure 会呈现出其独特的一些东西。但类似于每一种被授予“函数式”称号的程序设计语言,Clojure的实现给我们提供了一个不同的视角,让我们有机会审视自己之前的经验。如果你完全不熟悉函数式编程的技术,第7章可能是令人费解的。以对象层次结构和命令式程序设计技术为核心的语言里,函数式编程的概念犹如异类。但我们相信,由于 Clojure 的决策源自函数式范型的编程模型,它应该是个正确的方向,我们希望你也会赞同。

大规模设计

任何规模的应用都可以把 Clojure 当做主要的语言,第8章关于宏的讨论会改变我们对于开发软件的想法。作为一种Lisp,Clojure也拥抱了宏,我们会带你经历理解宏的过程,让你意识到,能力越大,责任越大。

在第9章里,我们会带你领略 Clojure 内建的对“代码和数据”进行组合及关联的机制。从命名空间到多重方法到类型和协议,我们会逐一解释 Clojure 如何促进大规模应用的设计和实现。

Clojure 是一种共生的程序设计语言,这意味着,它要运行于宿主环境之上。目前选择的宿主是 Java 虚拟机,但未来,Clojure 可能会变成跨宿主平台。无论如何,Clojure都会提供一流的函数和宏,用于与宿主平台的直接交互。

在第10章里,我们会讨论 Clojure 与其宿主互操作的方式,自始至终关注于JVM。

Clojure 与生俱来对程序状态就有着完善的管理,简化了并发程序设计,这些内容会在第11章看到。Clojure 的状态模型简单而强大,缓和了这种复杂任务所包含的大多数头疼问题,我们会逐个为你展示如何以及为什么使用。此外,我们还会强调一些并非由 Clojure 直接解决的问题,比如,如何识别和降低对 Clojure 引用类型所保护元素的需要。

杂项考量

本书最后一部分讨论了同样重要的话题:透过Clojure哲学的视角看待我们应用的设计和开发。在第12章里,我们会讨论改善单线程应用性能的一些方式。Clojure提供了许多机制改善性能,我们会逐一深入,包括其用法及适用范围。作为本书的总结,在第13章里,我们强调了在某些偏离开发行为的方面,Clojure改变了我们思考的方式,比如定义自己的应用领域语言、测试、错误处理和调试。

代码约定

本书的源码都采用直白而实用的方式进行了格式化。文本里内联列出的源码,比如(:lemonade :fugu),都采用等宽字体并加粗。在代码块里列出的代码片段距左边有一些偏移,采用等宽字体并加粗以突显出来:

如果源码片段表示一个表达式的结果,那么结果会有个前缀——“;=>”。这种特殊的序列有三重目的:

■ 有助于将结果从代码表达式中突显出来。

■ 它表示一个Clojure 注释。

■ 因为如此,可以轻松地将整个代码块从本书的EBook或PDF版本中复制出来,粘贴到Clojure REPL里运行:

此外,在REPL里,如果预期的显示不是返回值(比如表达式或打印输出),那么,实际的返回值前面会有个先导的“;”:

在上面的例子里,显示为{:user/zombies 2700, :user/humans 9}的map 就是个打印值,而nil表示println函数的返回值。如果表达式后面没有显示返回值,那么我们可以认为就这个例子而言,要么是nil,要么是忽略了。

阅读Clojure 代码 阅读Clojure 代码,如果从左向右阅略读,只要注意重要部分的上下文(defn、binding、let等)即可。而由内而外阅读,则要仔细注意每个表达式返回的东西,以传给紧邻的外部函数。在阅读最内部的表达式时,记住整个外部上下文会让事情简单许多。

无论是内联,还是代码块,所有格式化过的代码都是为了让键入或粘贴同写进Clojure源码或REPL的完全一样。一般来说,Clojure的提示符user>没有显示,因为它会导致复制/粘贴的失败。最后要说的一点是,我们有时会用省略号...表示略去的结果或打印输出。

在许多列表里还有一些代码标记,强调了一些重要概念。某些情况下,还会有一些用以解释的数字编号链接跟在列表后面。

获取Clojure

如果你目前还没装Clojure,那么,我推荐你使用David Edgar Liebke 创建的ClojureREPL包(Cljr),位于http://joyofclojure.com/cljr,其安装按如下指令进行。

先决条件

■ Java 1.6 及以上版本

■ 互联网连接

指令

在操作系统的控制台里运行如下命令:

如果你下载的Cljr包是以.zip文件为扩展名,运行方式如下:

这样,你会看到Cljr安装和包下载的进度输出。完成之后,你会看到类似于下面的指令,提示你运行Cljr:

遵循所示命令,即可运行Cljr。

REPL

Cljr包会运行1.2.0版的Clojure REPL(读取/求值/打印循环,Read/Eval/Print Loop)——这正是本书对应的版本。启动Cljr程序之后,你会看到如下图所示的窗口。

Cljr REPL类似于标准的Clojure REPL,只是额外增加了一些便于使用的特性,详情参见http://github.com/fogus/cljr。

本书并不假定你用了Cljr,但无论你的个人REPL设置是怎样的,应该都可以——只要运行的是Clojure 1.2 版。

下载示例代码

本书所有的可运行源码都可以从出版商的网站下载,www.manning.com/TheJoyofClojure。

作者在线(Author Online)

购买本书的同时,你也就可以免费访问Manning Publications的一个私有Web 论坛,可以在上面发表评论,咨询技术问题,得到作者和其他用户的帮助。要访问和订阅这个论坛,请到www.manning.com/TheJoyofClojure。注册之后,就可以从这个页面获得论坛访问信息,了解得到怎样的帮助,以及论坛的管理规则。

Manning对读者的承诺是,提供一个场所,让读者之间以及读者和作者之间进行一场有意义的对话。作者方面无法承诺参与的量,在AO上面的贡献是志愿的(且无偿)。我们建议你尝试问作者一些有挑战性的问题,以免他们意兴阑珊。

只要本书在售,作者在线论坛和之前讨论的合集就都可以在出版商的网站上访问。

关于封面插图

本书封面图片的标题为“自信的人”,在19世纪的法国,这样的造型可能是医师,也可能是巫医,可能是老千,也可能是钱庄老板,还可能是行商。本插图取自 19 世纪版的Sylvain Maréchal 四卷本的地区着装风俗纲要(出版于法国)。每幅插图都是由手工绘制上色的。Maréchal藏品中丰富的多样性生动地提醒着我们,仅仅距今 200 年前,世界上不同城镇地区在文化上竟迥然不同。人们彼此远离,说着不同的方言和语言。无论在街道还是乡村,仅仅通过着装便能很容易地识别出他们生活的地方以及他们的职业。

自此之后,着装产生了变化,曾是那样丰富的区域差异逐渐消逝。现如今,已经很难区分出不同国家的居民,遑论城镇地区。也许,我们是用文化差异换得了更具多样性的人生——肯定是更具多样性,更快速进步的高科技人生。

在这个已经很难区分计算机书与其他图书的时代,Manning热衷于创新,首创了以两世纪前地区生活丰富差异性为封面的计算机业务,将我们带回到 Maréchal 图片上描绘的生活中去。

译者简介

郑晔,ThoughtWorks公司首席咨询师,人生目标是终身编程,沉浸软件行业十余年,热衷于探索各种程序设计语言在真实软件开发中所能发挥的威力,致力于探寻合理的软件开发方式。加入 ThoughtWorks 公司后,曾畅游形形色色的软件开发项目,品味林林总总的开发语言。做过演讲,也写过文章,翻译过书,也贡献过开源,愿意与人畅聊技术,也愿意分享自己的经验。他的blog是梦想风暴(http://dreamhead.blogbus.com/),新浪微博是@dreamhead。

第1部分 基础

即便最宏伟的建筑也必须从坚固而粗鄙的基础开始。我们也要从浇筑知识基础起步,有了这样的基础,才能对Clojure中那些不甚熟悉的做法产生深刻的理解。这样的基础包括,Clojure根本的程序设计哲学、由数据和函数构建起的坚固围墙、REPL以及nil双关,当然,还有其他一些东西。

 

第1章 Clojure 哲学

本章包括的内容:

■ Clojure之道

■ 为何(又一种)Lisp

■ 函数式编程

■ Clojure为何不是面向对象的

通常说来,学习新语言要在智力和精力上都有极大的投入,只有程序员预期所学语言能够物有所值,这样的投入才是公平的。Clojure 出自 Rich Hickey 的手笔,他试图规避使用传统面向对象技术管理可变状态带来的诸多复杂性:既有本质的,也有偶然的。凭借对程序设计语言严肃研究而进行的贴心设计,以及对实用性的热切追求,Clojure 逐渐发展为一门重要的程序设计语言。它正扮演起一个不可忽视的重要角色,体现着程序语言设计的最新发展方向。在等式的一边,Clojure 充分利用了软件事务性内存(Software Transactional Memory,STM)、代理(agent)、标识和值类型之间的清晰划分、任意的多态以及函数式编程等诸多特性,总的来说为理清状态提供了一个有益的环境,尤其是在并发方面。另一方面,Clojure同Java虚拟机是一种共生的关系,这样一来,程序员们可以利用既有的程序库,而不必维护另一套基础设施。

在程序设计语言的历史长河中,Clojure只是一个婴儿,但其用法(或是说“最佳实践”或惯用法)却源自有50年历史的Lisp [1],以及有15年历史的Java。此外,自问世以来,其热情的社区就呈现出爆炸式增长,培育出自己独特的一套惯用法。正如前言中提及,一种语言的惯用法让我们可以用简洁的形式表现更复杂的东西。虽然我们肯定会涵盖Clojure的惯用代码,但我们还想进一步探讨语言本身“为什么”要设计成这样。

在本章中,我们会讨论一些既有语言的缺陷,这也是Clojure设计要解决的,在这些领域里,Clojure有着怎样的优势,以及Clojure包含的诸多设计决策。我们还会看到一些既有语言对Clojure的影响,此外,还会定义一些整本书都会用到的术语。

 

1.1 Clojure之道

我们会慢些起步。

Clojure是一门观点鲜明的语言,它并不打算涵盖所有编程范式,也不准备提供清单列出每个重要特性。相反,它只提供以 Clojure 之道解决各种真实问题所需的特性。要从 Clojure 中获得最大收益,我们就要写出遵循语言自身愿景的代码。在本书中,我们会依次介绍语言特性,但我们想说的并不只是一个特性做了些什么,更重要的是,为什么会有这样的特性,以及如何利用好这样的特性。

但是,开始之前,我们先来从宏观上了解一下Clojure最重要的哲学基础。图1.1列出了 Rich Hickey 设计 Clojure 时头脑中一些大致的目标,以及为了支持这些目标而内建在语言中的一些更具体的决策。

图1.1 Clojure的大致目标:本图展示了构成Clojure哲学的一些概念,以及这些概念之间的交互

如图1.1所示,Clojure的总目标由一些支持目标和功能综合而成,稍后几节,我们会逐一谈及。

 

1.1.1 简单

复杂问题很难有一个简单的解决方案。但是,如果把事情搞得不必要的复杂,即便是有经验的程序员也会栽倒,这就是“偶然复杂性”,与其相对的是任务的本质复杂性(Moseley 2006)。Clojure 致力于帮我们解决各种复杂问题,而不引入偶然复杂性,比如,各种数据需求、多并发线程、独立开发的程序库等。它还提供了一些工具,减少了一些初看起来像本质复杂性的东西。如此一来,最终的特性集合或许看起来并不简单,尤其在我们对这些特性还不甚熟悉时,但随着通读本书,我们认为,你会逐渐体会到Clojure去除了多少的复杂性。

偶然复杂性有一个例子,就是现代面向对象程序设计语言的一个发展趋势,即它要将所有可运行代码打包在类定义、继承和类型声明这样的层次里。Clojure通过支持“纯函数”去除了所有这些东西,所谓纯函数就是传入几个实参,然后,只根据这些实参产生一个返回值。Clojure很大一部分就是构建在这样的函数基础上的,绝大多数应用也可以如此,这意味着,尝试解决手头问题时,需要考虑的东西会更少。

 

1.1.2 专注

写代码总是要和干扰做斗争,每当语言让我们思考语法、运算符优先级、继承层次结构时,只会让干扰增多。Clojure尽力让一切保持尽可能简单,无需为探索一个想法经历“编译—运行”的循环,无需类型声明,等等。它还提供了一些工具,让我们可以改造语言,使词汇和文法能够更好地适应问题领域,因此,Clojure极具表现力。这种做法影响极大,可以在不牺牲可理解性的前提下,很好地完成一些极其复杂的任务。

之所以能够保持专注,关键一点在于恪守对动态系统的承诺。Clojure程序中定义的几乎所有一切都是可以重新定义的,即便程序尚在运行:函数、多重方法、类型、类型层次结构,甚至Java的方法实现。动态重定义这些东西貌似很可怕,尤其是在生产系统上,但它却为思考如何编写程序打开了另一种奇妙的可能性。我们可以对不熟悉的API进行更多的实验和探索,这是一种乐趣,而这种乐趣却常常为更静态的语言、漫长的编译周期所阻碍。

但是,Clojure 并不只有乐趣。乐趣只是一种副产品,更重要的是,它可以让程序员有能力获得超乎想象的高效。

 

1.1.3 实用

某些程序设计语言生来只为展示学术成果,或是探索某种计算理论。Clojure不在此列。Rich Hickey曾在很多场合说过,在构建有趣且有用的应用方面,Clojure是很有价值的。

为达此目标,Clojure 努力做到务实——一种用于完成工作的工具。在Clojure 里,如果某一设计决策要在实用和聪明、花哨或是纯理论的解决方案进行权衡,胜者往往是那些实用的解决方案。Clojure曾试图让我们远离Java,但这样做要在程序员和程序库之间插入大量API,这种做法可能会让第三方Java程序库很难用。所以,Clojure选择了另一条路:不做封装、编译成相同的字节码,能够直接访问Java的类和方法。Clojure字符串就是 Java 字符串;Clojure 函数调用就是 Java 方法调用。这样做简单、直接、务实。

使用Java虚拟机(JVM)这个决策本身就是一个务实的做法。JVM存在某些技术上的缺陷,诸如启动时间、内存使用、缺乏尾递归优化(tail-call optimization,TCO) [2]。但是,它也是一个惊人的务实平台——成熟、快速、部署广泛。其支持各种硬件和操作系统,拥有数量众多的程序库,以及支持工具,由于这个极尽务实的决策,所有这一切都可以为Clojure所用。

除了直接的方法调用外,Clojure还有proxy、gen-class、gen-interface(参见第10章)、reify、definterface、deftype 和defrecord(参见9.3节),为互操作性提供了许多选择,所有这些都是为了完成工作。务实对Clojure很重要,当然,许多其他语言也同样务实。我们后面会看到Clojure摆脱混乱的一些做法,正是这些地方让它显得与众不同。

 

1.1.4 清晰

When beetles battle beetles in a puddle paddle battle and the beetle battle puddle is a puddle in a bottle they call this a tweetle beetle bottle puddle paddle battle muddle. [3]

—Dr. Seuss

下面有一段简单的代码,可能是用Python写的:

执行这段代码之后,x的值是什么呢?如果假设process没有改变x的内容,那就应该是[6],对吧?但是,怎样才能做这样的假设呢?如果不了解process做了些什么,调用了怎样的函数等,我们根本无法确认。

就算process不会改变x的值,这时,再加入多线程,我们还是会有一大堆顾虑。如果在第一行和第三行之间,另一个线程改变了x会怎么样?还有更糟糕的,如果在第三行做赋值时,某个东西设置了x,那又该如何?你能保证你的平台写变量是原子操作吗?或者,是不是最终的值可能是多个写操作混杂的结果?我们可以抱着获得某种清晰的想法,将这个思维训练无休止地进行下去,但结果是一样的——我们根本无法得到清晰,只会适得其反:混乱。

Clojure为代码的清晰做着努力,提供了一些工具规避几种不同的混乱。就刚才描述的那种情况而言,采用它所提供的不变局部量和持久化集合,便可一并消除了单线程和多线程的大部分问题。

当我们所用的语言将不相关的行为合在一个构造里时,我们不难发现,自己已深陷多种泥潭。Clojure通过分离关注点让我们保持警醒,应对这样的情况。一旦事物得到分离,思路就会清晰许多,只在必要时重新组合。从某种程度上说,这样的做法对某些特定问题非常有用。表1.1将某些语言把概念混杂在一起的常规方式,同Clojure类似概念分离的做法进行了对比,本书稍后会对Clojure的做法进行更详尽的解释。

表1.1 Clojure中的分离关注点

有时,很难在脑子里将这些概念区分开来,但如果能做到的话,就会非常清晰了,为了这种强大和灵活,我们值得努力一试。我们有那么多不同的概念要处理,以一致的方式表现代码和数据就显得很重要了。

 

1.1.5 一致

Clojure在两个具体的方面提供了一致性:语法和数据结构。

语法一致性指的是,相关的概念在形式上是类似的。有个简洁有力的例子,for 和doseq这两个宏之间的语法是一样的。

它们做的事情不尽相同——for返回的是一个惰性seq,而doseq只是为了产生副作用——但二者支持相同的迷你语言(mini-language):嵌套迭代、解构、:when 和:while卫语句。比较下面这个例子就不难看出相似性:

这种相似的价值在于,只要学习一种基本语法即可应对两种情况,必要时,在两种用法间切换也会容易许多。

类似地,数据结构的一致性表现在 Clojure 持久化集合类型的精心设计上,它为各个类型提供了尽可能相似的接口,并尽可能广泛地去使用这些接口。这种做法实际上是Lisp经典的“代码即数据”哲学的扩展。Clojure数据结构不只可以持有一大堆应用的数据,还可以持有应用自身的一些表达式元素。它们可以描述对 form 的解构,还可以为各种内建函数提供命名选项(named options)。其他面向对象语言可能会鼓励应用定义多个彼此不相容的类,以持有不同类型的应用数据,而 Clojure 则鼓励使用行为上类似于map的对象。

这样做的好处在于,同样一套处理Clojure数据结构的函数可以用于下列所有情形:大规模数据存储、应用代码和应用数据对象。用into可以构建任意类型,用seq可以获取一个用于遍历的惰性seq,用filter可以选择满足特定条件的元素,等等。一旦习惯了所有这些丰富且随处可用的函数,用Java或C++处理应用中的Person或Address这样的类就会让人觉得处处掣肘。

简单、专注、实用、一致和清晰。

Clojure程序设计语言里几乎所有元素都是为了提振这些目标。编写Clojure代码处理真实问题时,请将“简单、实用、专注”等方面推向极致,将此铭记于心,我们相信你会发现,Clojure就是你到达成功彼岸所需的工具。

 

1.2 为何(又一种)Lisp

一套好的概念可以将大脑从无用功中解放出来,专注于更高级的问题。

—Alfred North Whitehead

去到任何一个开源项目托管站点,搜索“Lisp interpreter”(Lisp解析器)。这个貌似平淡无奇的搜索,可能会带给我们一个堆积如山 [4]的结果。事实上,计算机科学的历史中堆积着大量废弃的Lisp实现(Fogus 2009)。诸多初衷良好的Lisp来了又走,一路遭到无数嘲笑,但到了明天,搜索结果依然会无限制增长。既然有如此惨痛的过往,为何还有人愿意将其崭新的程序设计语言构建于Lisp模型之上呢?

 

1.2.1 优美

Lisp吸引了计算机科学史上最聪明的一群头脑。但是,仅有权威的争论是不够的,我们不该仅凭此判断Lisp。只有用其编写应用,才可以直接看得到Lisp语言家族的真正价值。Lisp的风格就在于极具表现力、非常实用,以及在大多数情况下表现出的美感。最初的Lisp 语言是John McCarthy在其惊天动地的论文“Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”(McCarthy 1960)中定义的,只用区区7个函数和两个特殊form便定义出整个语言:atom、car、cdr、cond、cons、eq、quote、lambda和label。

通过这9个form的组合,McCarthy将整个计算以一种令人窒息的方式呈现出来。计算机程序员总在寻找美,而多数情况下,美会以简单的形式自我呈现出来。7个函数2个特殊form,美无过于此。

 

1.2.2 极度灵活

Lisp何以历经五十余年而弥新,相较之下,无数语言却成了匆匆过客?个中原因可能极尽复杂,但究其根因,无外乎Lisp 自身的语言基因(Tarver 2008)将语言的灵活性推向极致。Lisp新手常常气馁,无处不在的括号和前缀记法,与非Lisp程序设计语言大相径庭。然而,正是这种行为上的规律性,不仅让需要记忆的语法规则减少了,也让宏的编写变得很简单。我们会在第8章更详细地了解宏,但为了让你开开胃,这里先简单地看一下。下面是一个例子,稍后再来细究:

希望你对这些单词有所了解,因为这不是一本 SQL 的书。这里要说的是,Clojure并没有内建对 SQL 的支持。SELECT、FROM 等这些词并不是内建的 form。它们也不是常规的函数,如果SELECT是,那么使用a、b和c就错了,因为它们还没有定义。

如何用Clojure定义这样的领域特定语言(domain-specific language,DSL)呢?好吧,这不是产品就绪(production-ready)的代码,没有绑定到真实的数据库服务器上;但只要有了程序1.1列出的一个宏和三个函数,前面的查询就能够返回下面这些实际的值:

请注意,FROM和ON这样的词是从输入表达式中直接取出来的,而其他诸如~max和AND则要特殊对待。调用查询时,max得到一个5,这是从字面量SQL字符串中提取的,由一个单独向量提供,以这种方式准备的查询颇为完美,可以免受 SQL 注入攻击。AND form由Clojure 的前缀表达式转成SQL 所需的中缀表达式。

程序1.1 以Clojure编写领域特定语言,用以嵌入SQL查询

但需要指出的是,这算不上是一种很好的SQL DSL——还有实现更为完整的。 [5]我们要说的是,一旦懂得了这种创建DSL的技巧,就可以识别出一些机会,定义自己的DSL,解决比SQL更窄的、更加面向应用的问题。无论是给不常见的非SQL数据库提供查询语言,还是给模糊的数学学科提供一种方式表现函数,抑或是处理其他自己都未曾想过的应用,能够拥有如此灵活易扩展的基础语言,且不损伤语言自身特性,都将成为游戏规则的改变者。

虽然我们不该太过深入细节地讨论实现,但还是要顺着之前讨论过的一些重要方面,简单看看列表1.1的实现。

自下而上阅读,首先映入眼帘的是入口点,SELECT宏。它返回的是一个有两项的vector——第一项通过调用 expand-clause 生成,返回的是一个经过转换的查询字符串,而第二项是另一个vector,表示输入里由~标记的表达式。~表示反quote,我们会在第8章讨论其更常见的用法。另外要注意的是这里用到的tree-seq,通过它可以很容易地将感兴趣的项从值树(也就是输入表达式)上提取出来。

expand-clause函数用语句的第一个词,在clause-map里进行了查询,然后,调用适当的函数,完成从Clojure的s表达式(s-expression)到SQL字符串的转换。clause-map为SQL表达式各个部分提供了所需的详细功能:插入逗号或是其他SQL语法,有时还要递归调用 expand-clause 进行子语句的转换。其中之一是 WHERE 语句,通过委托给expand-expr函数,处理了SQL所需的前缀表达式到中缀表达式的通用转换。

总的来说,这个例子展示的Clojure灵活性大多是因为宏可以接受代码form(比如前面展示的这个SQL DSL的例子),并将其当做数据对待——遍历树、转换值等。之所以可以这样做,不只是因为代码可以当做数据,还因为在Clojure程序里,代码就是数据。

 

1.2.3 代码即数据

“代码即数据”这样的说法最初很难理解。实现一门程序设计语言,代码同数据一般对待,这需要语言本身具有非常强的可塑性。当语言就是以这种本质的数据结构表现时,语言本身就可以操作自己的结构和行为了(Graham 1995)。读到上面这句话,我们脑海中可能会浮现出一条衔尾蛇(Ouroboros) [6],也许这么说不合适,因为Lisp可以比作一个自我舔食的棒棒糖——更正规的说法是同像性(homoiconicity)。要完全掌握Lisp的同像性,需要跨越一个巨大的概念鸿沟,但在本书里,我们尽力帮你理解这个概念,希望你最终能够领会其巨大的威力。

初学Lisp是一番乐趣,如果你能从本书得到同样的体验,那么我们欢迎你——甚至有点嫉妒。

 

1.3 函数式编程

快点回答,函数式编程是什么意思?错!

别太泄气,其实,我们也不知道确切的答案是什么。函数式编程只是诸多定义模糊的计算机术语 [7]中的一个。如果找100个程序员问它的定义,我们会得到100个不同的答案。确实,某些答案是类似的,但如同雪花一般,没有两个答案是完全一样的。要进一步搅混水的话,让计算机科学的专家们单独给出定义,我们可能会发现,某些答案甚至是彼此矛盾的。同样,任何一个函数式编程定义的基本结构都可能会不同,这完全取决于回答问题的人喜欢用哪种语言写程序:Haskell、ML、Factor、Unlambda、Ruby、Qi等。随便一个人、一本书或是一门语言怎么就能声称自己是函数式编程的权威呢?然而,正如大多数各具特色的雪花都是由水组成的,各种说法的函数式编程核心都遵循着同样的核心原则。

 

1.3.1 一个可行的函数式编程定义

无论函数式编程定义用的是lambda 演算、单子I/O(monadic I/O)、delegate 还是java.lang. Runnable,基本的单元可能就是某种形式的过程、函数或是方法——这是根本。函数式编程关心和处理的是函数的应用和组合。再进一步,一门被认为是函数式的语言,它的函数概念一定是一等的。在这门语言里,函数可以存储、可以传递,还可以返回,同语言里的其他数据一样。各种不同的定义远远超出了这一核心概念,但是,谢天谢地,作为起点,这足够了。当然,我们还会进一步阐述一下 Clojure 风格的函数式编程,包括纯粹性、不变性、递归、惰性和引用透明等主题,不过,这些东西稍后会在第7章讨论。

 

1.3.2 函数式编程的内涵

一般说来,面向对象程序员和函数式程序员看到问题和解决问题的方式有所不同。面向对象思维模式采用的方式是,把应用领域定义成一组名词(类),函数式思维模式则会把解决方案视为各种动词及其组合(函数)。虽然二者产生的结果可能是一样的,但函数式解决方案会在简洁、可理解、可重用方面更胜一筹。确实如此!希望在本书结束时你也会认同,函数式编程会让程序设计更为优雅。这是一种思维模式的转换,从考虑名词,到思考动词,但这个旅程物有所值。无论如何,我们都相信,Clojure会让你获益良多,反哺到你选择的语言中——唯有打开心胸,方能体会这一点。

 

1.4 Clojure为何不是面向对象的

优雅同熟悉正交。

—Rich Hickey

Clojure的出现源自一种无奈,很大程度要归因于并发编程复杂性以及面向对象程序设计在这方面的无能为力。本节将会探索这些缺陷,了解 Clojure 之所以是函数式而非面向对象的根因。

 

1.4.1 定义术语

开始之前,先定义术语 [8]

第一个要定义的重要术语是时间(time)。简单说来,时间是指事件发生的相对时刻。有了时间,同实体关联在一起的属性——无论是静态还是动态,单数的还是组合的——会形成一种共生关系(Whitehead 1929),从逻辑上说,可以认为是其标识(identity)。在任意给定的时间,都可以得到实体属性的快照,这个快照定义了状态(state)。在这种概念里,状态是不可变的,因为状态没有在实体本身内定义成一种可变的东西,只是展现某个给定时刻的属性而已。想象一下,有一本儿童手翻书,如图1.2所示,完全是为了理解这些术语。

有一件事很重要,要特别提一下,在面向对象程序设计的加农炮里,状态和标识并没有清晰的区分。换句话说,这两个概念合并成一个通常称为可变状态的东西。经典的面向对象模型对对象属性的修改毫无限制,完全不会保留历史状态。Clojure的实现尝试在对象状态和标识(因为其与时间有关)之间画出一条清晰的界限。同样是上面手翻书的例子,采用可变状态模型结果是完全不同的,为了表示与 Clojure 模型之间的差异,可以参考图1.3所示。

图1.2奔跑者:儿童手翻书,用以解释Clojure状态、时间和标识的概念。书本身表示标识。当我们希望插图有所改变时,就画另一幅图,加到手翻书的末尾。翻动书页的动作,表示状态随时间改变。停到给定页面,观察特定图片,表示某一时刻奔跑者的状态
图1.3可变的奔跑者:将状态改变建模为可变的,需要准备一些橡皮擦。书只有一页,状态改变时,我们必须物理擦除,根据修改重绘图片的一部分。采用这样的模型可以看出,可变性摧毁了时间、状态和标识这些概念,变成了只有一个

不变性是Clojure的基石,Clojure实现的绝大部分都是为了高效地支持不变性。通过关注不变性,Clojure完全消除了可变状态(这是一个矛盾修辞法 [9])的概念,这说明大多数对象表示的东西其实都是值。从定义上说,值是指对象固定不变的代表值 [10]、量级或是时间段等。或许,你会问自己:在Clojure里,这种基于值的编程语义内涵到底是什么呢?

很自然,遵循严格的不变性模型,并发一下子就变成一个比较简单(虽然还是不那么简单)的问题,这意味着,如果不必顾忌对象状态的改变,我们便可肆无忌惮地共享,而无惧并发修改。我们会在第11章看到,Clojure把值的修改与其引用类型隔离开来。Clojure的引用类型为标识提供了一个间接层,这样一来,标识就可以用于获得一致的状态,如果不总是当前状态的话。

 

1.4.2 命令式“烘烤”

命令式编程是如今占主导地位的编程范式。命令式程序设计语言最纯粹的定义是,这种语言用一系列语句修改程序状态。在编写本书期间(很可能也是未来一段时间内),命令式编程首选的风格就是面向对象的风格。这样的事实本质上没那么糟糕,因为无数成功的软件项目就是用面向对象命令式编程技术构建的。但在并发编程的上下文里,面向对象命令式模型却是自我吞食 [11]的。命令式模型允许(甚至鼓励)无限制地修改变量,所以,它并不直接支持并发。如果对修改的不加控制,那么任何变量都无法保证包含的值是符合预期的。面向对象程序设计将状态聚合在对象内部,朝着这个方向又迈了一步。虽然加锁机制让单个方法可能是线程安全的,但是,如果不采用更为复杂的加锁机制,并扩大加锁范围,就没有办法在多个方法调用间保证对象状态的一致性。而Clojure则关注于函数式编程、不变性,注意区分状态、时间和标识。当然,面向对象并没有彻底失去希望。实际上,它在很多方面还是可以促进编程实践的。

 

1.4.3 OOP 提供的大多数东西,Clojure 也有

有一点应该清楚,我们并不打算流放面向对象程序员。相反,要提升自己的技艺的话,了解面向对象程序设计(OOP)的缺陷是很重要的。在后面几小节里,我们也会接触到OOP一些强大的方面,让我们看看用Clojure如何来做,以及在某些情况下,Clojure给出了怎样的改进。

1.多态和表达式问题

多态是这样一种能力,函数或方法根据目标对象类型的不同有着不同的定义。Clojure也有多态,它是通过多重方法和协议实现的,相比于许多语言中的多态,这两种机制都更开放,扩展性更好。

在程序1.2中,我们定义了一个叫做Concatenatable的协议,包含了一个或多个函数(这里只有一个,cat),定义出其提供的函数集。这意味着cat函数对任何满足协议Concatenatable的对象都起作用。之后,我们用这个协议扩展String类,给出一个特定的实现——函数体将一个实参other连到了字符串this上。我们还可以用这个协议扩展其他类型:

程序1.2 Clojure的多态协议

至此,这个协议已经扩展到两个不同的类型上,String和java.util.List,所以,无论用哪个类型作为第一个实参调用cat函数——都会调用到相应的实现。

注意,String在定义协议之前就已经定义过了(对这个例子而言,是由Java本身定义的),而我们依然可以成功地用新协议扩展它。对很多语言而言,这是不可能的。比如,Java 需要我们定义好所有的方法名及其分组(称之为接口),然后才能用一个类实现它们,这种限制称为表达式问题。

表达式问题 表达式问题指的是,在不修改已定义代码的前提下,为既有具体类实现一套既有的抽象方法。面向对象语言允许我们在自己控制的具体类里实现既有的抽象方法(接口继承),但是如果具体类不在控制范围内,实现新的或是既有抽象方法的办法就不那么多了。一些动态语言,比如Ruby和JavaScript,为这个问题提供部分的解决方案,我们可以给既有的具体对象中添加方法,这种特性称之为猴子补丁(monkey-patching)。

Clojure的协议可以扩展到任何有意义的类型上,甚至是类型原实现者或是协议原设计者想都没想过的地方。我们会在第9章更深入地探索Clojure风格的多态,不过,这里还是希望你能对于其运作机理有个大概的了解。

2.子类型化和面向接口编程

Clojure可以创建一种特殊的层次结构,提供了一种子类型化的方式。稍后,我们会在9.2节深入研究如何利用这种特殊的层次结构。类似地,Clojure通过其协议机制还提供了一种类似于Java接口的能力。通过定义一套逻辑分组的函数,我们就可以开始定义协议,这是数据类型抽象必须要有的。面向抽象的编程模型是构建大规模应用的关键,我们会在9.3节及随后的章节了解到。

3.封装

如果 Clojure 不以类来组织,那如何进行抽象呢?想象一下,我们需要一个简单的函数,对于给定的棋盘和坐标,返回给定方格上棋子的一个简单表示。为了让这个实现尽可能简单,我们用一个包含一套字符的vector表示不同颜色的棋子,如程序1.3所示。

程序1.3 用Clojure表示的简单棋盘

没有必要把棋盘弄得更复杂了;象棋就够难的了。在代码中,这个数据结构直接对应着实际的棋盘,从起始点开始,如图1.4所示。

图1.4 相应的棋盘布局

从图1.4中可以看出,黑棋是小写字符,白棋是大写的。这种结构可能不是最优的,却是个好的开始。我们暂且忽略实际的实现细节,关注于查询棋盘某块的客户端接口。要强制封装,以免客户端代码陷入棋盘实现细节,这是个绝佳的机会。幸运的是,拥有闭包的语言自动就支持某种形式的封装(Crockford2008),把函数根据其支持的数据进行分组 [12]

程序 1.4 所列的函数本身的意图不言而喻 [13],通过使用defn宏,创建命名空间内私有的函数,将函数封装在命名空间joy.chess的某个层次里。在这个例子里,使用lookup函数的命令是这样的:(joy.chess/lookup (joy.chess/initial-board) "a1")。

程序1.4 查询棋盘的某个方格

探索惯用的源码不难发现,Clojure的命名空间封装是最为普遍的一种封装方式。但是,如果采用词法闭包,则有更多的封装选择:block级封装,如程序1.5所示,以及局部封装,二者都能有效地将一些不甚重要的细节聚合在更小作用域里。

程序1.5 使用block级封装

在最明确的作用域内聚合相关数据、函数和宏通常都是个好主意。我们依然可以像之前一样调用lookup,但是,在更大的作用域内,辅助函数是不可见的——在这个例子里,就是命名空间 joy.chess 里。前面代码的 file-component 和 rank-component 函数,*file-key*和*rank-key*值都从命名空间里挪了出来,放到了由 letfn 宏定义的 block 级index函数里。随后,我们在这个宏体内定义了lookup函数,这样就限定了暴露给客户端的棋盘API,隐藏了特定函数和form的实现。但是,我们还可以进一步限定封装的作用域,正如程序1.6所示,进一步收缩作用域,使之成为真正的函数局部的上下文。

程序1.6 局部封装

终于,我们把所有实现相关的细节都放到 lookup2 本身的函数体里。这就将 index函数和所有辅助值的作用域都限制在了相关的地方——lookup2。额外的奖赏是,lookup2简单而紧凑,没有牺牲任何可读性。当然,Clojure避开了大多数面向对象语言浓墨重彩表现的数据隐藏封装的概念。

4.并非万物皆对象

最后要说一点,面向对象程序设计的另一个不足之处是,函数和数据之间绑定过紧。事实上,Java程序设计语言强迫我们把整个程序完全构建在类层次结构上,所有功能都必须出现在高度受限的“名词王国”(Yegge 2006)所包含的方法中。这一环境如此受限,以致于程序员们只能被迫闭上双眼,否则将无法面对这些组织不当的方法和类带来的尴尬结果。正是因为这种极尽苛刻的以对象为中心的视角,导致了Java代码显得啰嗦而复杂(Budd 1995)。Clojure的函数就是数据,然而,对于数据及处理数据的函数而言,并不要求一定要将二者解耦。许多程序员认为是类的东西,实际上就是Clojure用map [14]和记录形式提供的数据表。对“视万物为对象”的最后一击是,在数学家眼里,没有什么东西是对象(Abadi 1996)。相反,数学通过应用函数,构建于一组元素同另一组元素之间的关系基础之上。

 

1.5 小结

我们在本章讨论了很多概念性的东西,但这是必需的,定义了本书余下部分用到的术语。类似地,要进行后续讨论,理解 Clojure 的基础是很重要的。如果你能从之前的章节了解这些内容,甚至内化为自身的一部分,那么恭喜你:你拥有了继续阅读本书的坚实基础。但如果你尚不确定自己是否了解Clojure,那也OK——我们可以理解,一次消化所有这些内容有点多。随着我们逐渐讲述 Clojure 的故事,这些东西慢慢就能理解了。如果有函数式编程的背景,之前章节的一些讨论,你很可能会感到很熟悉,或许也会有些令人吃惊的纠结之处。相反,如果你的背景更多地植根于面向对象程序设计,那你会觉得 Clojure 同自己熟悉的东西有着极大的不同。虽然在很多方面着实是这样的,但在后续章节里,你会见到 Clojure 怎样优雅地解决我们日常的问题。同经典面向对象技术相比,Clojure从一个截然不同的角度来解决软件问题,但是,正是面向对象这些基本的长处和缺点激励着Clojure不断前进。

有了这样的概念基础,是时候快速看看 Clojure 技术的基础和语法了。下面会过得很快,但也没快到理解后面章节都困难的份上。好吧,打开你的REPL,我们出发了……

图书在版编目(CIP)数据

Clojure编程乐趣/(美)福格斯(Fogus,M.),(美)豪泽(Houser,C.)著;郑晔译.--北京:人民邮电出版社,2013.11

ISBN 978-7-115-31949-4

Ⅰ.①C… Ⅱ.①福…②豪…③郑… Ⅲ.①程序语言—语言设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2013)第099948号

版权声明

Simplified Chinese-language edition copyright ©2013 by Posts & Telecom Press. All rights reserved.

Original English language edition, entitled The Joy of Clojure, by Michael Fogus, Chris Houser , published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830. Copyright © 2011.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。

◆著 [美]Michael Fogus Chris Houser

译 郑晔

责任编辑 陈冀康

责任印制 程彦红 杨林杰

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

邮编 100061  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

三河市海波印务有限公司印刷

◆开本:800×1000 1/16

印张:21.5

字数:448千字  2013年11月第1版

印数:1-3000册  2013年11月北京第1次印刷

著作权合同登记号 图字:01-2012-6450号

定价:59.00元

读者服务热线:(010)67132692 印装质量热线:(010)67129223

反盗版热线:(010)67171154

相关图书

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

相关文章

相关课程