对《Clojure in Action》第二版的书评与作者问答

阅读数:1098 2016 年 6 月 7 日

话题:函数式编程Book Review语言 & 开发

由 Amit Rahore 及 Francis Avila 撰写,Manning 出版的《Clojure in Action》第二版以本质性的、通透的、结构组织良好的方式对 Clojure 1.6 进行了介绍。本书探索了该语言的核心部分内容,并为读者介绍了 Clojure 的编程与习惯。InfoQ 与本书的作者之一 Francis Avila 进行了一次访谈。

本书的第一版涵盖了 Clojure 1.2 版本的特性,而在第二版则针对 Clojure 1.6 版本进行了大幅更新。原先的几个章节中包含了一些外部库的介绍,而这些库有些已经被取代,或已经过时。因此这些部分在第二版中已经被删除,取而代之的是对 Java 与 Clojure 互操作性的关注。这表现了 Clojure 近几年间已经得到了更大范围的程序员社区,乃至非 Java 程序员的接受。

《Clojure in Action》旨在完整地涵盖 Clojure 的各方面细节,但也没有忽略它所依赖的 JVM 生态系统。整本书的编写方式可以看出一点敏捷的味道,主要体现在书中基本找不到没有意义的重复信息。本书以一种有趣的方式为读者展现了示例代码,许多代码都是直接取自于 Clojure 的标准库,例如对 Clojure 宏系统的讨论。因此这些代码非常接近于真实的 Clojure 代码。另一方面,作者在许多示例中尝试创建了一些抽象概念,这与 Clojure 中现有的抽象可谓相辅相成,例如对多态与协议的讨论。

本书共包括 11 个章节,完整地涵盖了 Clojure 语言的方方面面。首先介绍的是它的构建块,例如数据结构、函数、条件、命名空间等等,随后逐步开始介绍更高级的主题,例如 multimethod、宏与协议等等。本书的最后两个章节介绍了两个独特而又高度相关的主题:一是如何在测试驱动开发实践中应用 Clojure,其中详细地论述了单元测试、桩(stubbing)与模拟(mocking)技术。二是如何编写高级的宏与领域特定语言(DSL)。

InfoQ 与本书的作者之一 Francis Avila 进行了一次访谈,以进一步了解本书出版的情况、Clojure 的优点以及未来的发展。

InfoQ:你能否为我们介绍一下本书的写作动力来自何处?本书的必要性又体现在哪里?

Francis Avila:编写本书最基本的动力是帮助来自于更主流编程背景(Java、C#、Python、Ruby 和 JavaScript 等等)的程序员学习 Clojure 以及它的核心哲理:对于状态以及时间的管理、默认的不可变性、尽可能使用纯粹的函数式数据转换、以及将代码作为数据进行操作。在编写本书的第一版时,这些思想还没有成为主流,而 Amit Rathore 试图让人们了解 Clojure 的优势的呼声并没有引起人们的注意。对于 Clojure 有兴趣的人绝大部分都具有 Java 的背景,他们需要支持大型的 Java 生态系统,希望能够以一种更好的工具来管理这些复杂性。

第二版最主要的目的是更新书中的内容,以包含 Clojure 1.6 中的变更(第一版所针对的是 Clojure 1.2)。但这些年以来,编程领域已经发生了很大的变化:不可变性与函数式编程不再是离经叛道的思想了(尤其是对于动态类型的语言来说),并且越来越多来自 Java 世界之外的程序员开始对 Clojure 感到好奇。因此,这本书的必要性就在于为更大范围内的程序员介绍 Clojure 这门语言。令人啼笑皆非的是,这反而意味着本书在一定程度上变成了 Java 与 JVM 的辩护者,而这对于来自动态语言背景的程序员来说是一个不熟悉(并且抱有某种怀疑的态度)的概念。我知道这一点,是因为我自己就是这些程序员中的一员!

InfoQ:你能否简单地介绍一下你的知识背景,以及在 Clojure 方面的经验?你是通过怎样的机缘接触到 Clojure 的?

Francis:我的知识背景与计算机科学毫无关联,而是人文学科:包括哲学、神学、古希腊语以及古拉丁语。但我从小就是一位极客,在大学里自学了 Linux 与 Python。在毕业之后,我成为了一位 web 开发者,靠着 PHP 与其他一知半解的知识编写代码。我是从阅读了由 Bruce Tate 所撰写的《七周七语言》一书才认识 Clojure(本书介绍的七种语言之一)的。我对 Rich Hickey 提出的不可变性、状态以及复杂度等思想很感兴趣。随着我对 Clojure 的学习越深入,我越感觉到它证实了我在管理大型软件项目时被掩盖的一些直觉(在 PHP 项目中,如果你无法管理好它的复杂性,就会反过来被它牵着鼻子走!)。

之后我总算在我当时所在之处找到了一家招聘 Clojure 开发者的公司,我很快应聘了这个职位,并于 2012 年中段加入了 Breeze。Breeze 是一家位于路易斯安那州的小型电子健康记录公司,整个公司基本上只用 Clojure 和 ClojureScript 进行开发。

InfoQ:本书所针对的读者包括没有 Clojure 或其他函数式语言经验的开发者。你对于他们在阅读本书时有什么建议吗?他们可以期望从本书中得到哪些收获,而又有什么是他们不应期望的?

Francis:我能够建议的最重要一点就是在阅读过程中始终打开 Clojure REPL,并且在思考或遇到问题时进行一些尝试。读者可以挑战一些极端的情况,并观察一下发生了什么。如果你希望节省一点打字的时间,可以随意下载本书中的代码(为各章节设置了对应的命名空间以组织代码)。如果你能够找到一个集成了 REPL 的编辑器或 IDE,例如 Cursive(IntelliJ IDEA)、LightTable 或 Proto REPL(ATOM),那么整个过程会更为顺利。通过使用集成在编辑器中的 REPL,你可以在文件或缓冲中进行编辑,在编写代码的同时自动进行重新评估、重新定义与测试你的代码,而不是直接将代码复制粘帖至命令行中。

如果你遵循这些建议,那么在读完本书后,你就能够扎实地掌握编写 Clojure 代码的机制、如何从 Clojure 的角度处理问题、以及于何时何地应使用 Clojure 抽象工具中的哪一部分(例如在何时,以及如何使用一等函数、multimethod、协议、记录以及宏等等)。

不过,本书并不打算帮助你熟悉当前 Clojure 库的生态系统以及系统级别的模式。本书的第一版对于这一点投入了许多精力,但这方面的信息实在变化得太快。自打那时以来,大部分库都发生了极大的改变,或是被更好的技术完全取代了。因此,我们并不打算更新这些部分,而是缩减了这部分内容,更专注于如何开发 Clojure 的核心能力,将库的依赖方面的内容降至最低。

InfoQ:Clojure 中所提供的特性能够帮助开发者提高生产力,你能否以你的经验介绍一下这些特性?

Francis:通过 Clojure 提高生产力的最重要因素是它默认提供了丰富的不可变数据结构,让开发者不必再担忧“如果其他代码改动了这里怎么办”,或是“哪段代码改动了这里?”,这就大大地减少了 bug 的数量和为此浪费的脑细胞了。“专注小块功能”的编程方式比传统方式简单了许多,开发者可以专注于小块功能的开发,随后再将这些功能组合在一起。

让不可变性成为整个 Clojure 生态系统中默认的行为,对于其他的 Clojure 特性也带来了很大的影响,如若不然,那些特性本身就会显得暗淡许多。举例来说,Clojure 中提供的核心函数式编程结构(map、reduce 等等)让创建简单的、以数据为中心的转换管道变得更方便,但其他许多语言也提供了相同的功能(包括 Java!)。不过,一旦这种特性与不可变数据结构结合在一起,就能够发挥出惊人的威力,因为你可以随意地重新排列与检查管道中的数据,而不必担心造成无意中的 mutation(变动)。

InfoQ:本书用了大量的篇幅介绍宏编程以及 DSL,这已属于高端内容的范畴了。你能否说明一下为什么要在本书中介绍这种高端的话题呢?

Francis:宏是 Clojure 的 value proposition 特性的一个关键组成部分(同样也适用于 LISP),有效地利用宏是一项关键的技能。与其他语言中的常见技术相比,使用宏编写 DSL 将简便许多。宏的作用是控制在合法的 Clojure 程序中所嵌入的 form 的评估(在评估之前,代码即数据),否则的话,你将不得不从头开始完成各种任务,例如编写自己的语法、对应的记法分析器或解析器、AST 以及运行时。你可以利用 Clojure 运行时的完整功能,你所要做的主要工作就是添加自己的语法,并扩展至可执行的 Clojure 程序中。

此外,除了编写 DSL,宏还有更广泛的应用场景。如果你想要获取某个计算结果,而不是让代码去执行该计算(即“预编译”的值)、用内联代码取代函数调用、或是控制是否要评估某个代码块,以及控制评估的顺序(例如在某个控制流中),你就必须编写宏,因此了解他们的工作方式非常重要。

InfoQ:你曾提到 Clojure 能够将偶然的复杂性降至最低,能否举出几个实际的例子。所谓的“偶然的复杂性”是指什么,Clojure 又是怎样将其最小化的?

Francis:“偶然的复杂性”是我为那些由我们程序员本身造成的问题所取的名字。“复杂性”对应着“简单性”:简单的思想指的是,为了保持它的性质,不可从中去除任何一部分内容。而复杂的思想意味着仍然有部分内容是可以抛弃的。“偶然的”对应着“本质的”:复杂性并不属于要解决的问题的一部分(本质的),而属于解决该问题的方式的一部分(偶然的)。

Clojure 通过所提供的更简单的抽象方法避免了这种偶然的复杂性:这种“简单”并不是指“更易于使用”,而是指“没有任何多余的东西可以丢弃”。通过几个例子的对照可以便于我们理解这一点。

在大多数编程语言中,集合(例如一个引用数组)都是可变的:这表示它目前包含了某些成员,而之后可以包含不同的成员。这些集合是“复杂的”,因为他们的概念包括值(我包含了哪些成员),以及标识(我在内存中的地址是什么)和时间(我目前包含了哪些内容)。如果你试图共享这个集合,则使用者在改变其中的内容之前,必须小心地生成一个拷贝(因为它的值与标识是不可分割的)。如果你想获得该集合的一个快照,则必须在程序运行时拷贝整个集合的状态。

而在 Clojure 中,这三者将由不同的抽象概念所服务。集合(我一直在谈论的那种不可变的数据结构)仅仅是值的表现,因为他们不能够进行变更,并且与其他集合的等价性与他们在内存中是否具有相同的地址以及他们是否具有相同的底层实现无关。由于集合是不可变的,因此也是非时变(a-temporal)的:你可以“拥有”对整个程序在某一特定时间整个状态的引用,而这种状态是不可能发生变化的。它的值取决于你在哪个 box 中所输入的值。这些值在 Clojure 中有 4 种表现形式,每种形式都表现了随着时间的推移对变更进行管理的一种特定方式,例如异步与同步、协调与独立。这些值所在的 box 只包括标识,并且只对你所需的特定变更(例如时间)进行管理。

当你具备了一定 Clojure 的开发经验后,你就会察觉到多数问题都可以由值和纯函数解决,纯函数是指输入某些值,并生成另一些值的函数。只有少数场合下,你才需要 mutation(它必定要结合时间与标识)所带来的复杂性,在这种场合中,你需要使用 Clojure 中的某种引用类型(例如包含值的 box)。但是,在大多数编程语言中,无论你是否需要,你总要为时不时发生的 mutation 付出代价。这将成为一种偶然的复杂性,它与你处理问题的解决方案无关,而你仍然要对此进行管理。

如果你希望能够快速地了解这些核心思想(状态、值、时间、标识和行为),以及他们在 Clojure 与面向对象编程中的含义的对照,我建议你阅读一下这篇博客文章:“The unofficial guide to Rich Hickey’s Brain”。

InfoQ:假设有一位来自于命令式语言背景的程序员,他应该对 Clojure 的哪方面知识投入更多的专注?哪些技能是最难学习的,以及怎样能做到最好的学习效果?

Francis:最难学习的技能其实是一种“软”技能:即将你的程序想象为数据的转换,而不是操作或行为。训练这一技能的一种方式是经常性地观察你的函数并进行重构:

  • 如果函数中有副作用,就去掉它。
  • 如果出现了可变的 var/ref/atom(尤其是本地的),也去掉它。
  • 如果它需要调用某个集合,尝试着重写该函数,让它只调用集合中的一个元素,并使用 map 或 reduce(或其他有序函数)将这个函数变得通用化。
  • 如果你需要(通过 let)绑定一些中间结果,并且以一种特定的顺序使用,可以尝试着重新排列这些步骤或函数的签名,以线性化的宏取而代之。

InfoQ:如果某个公司打算采用 Clojure,能够期望从中获得哪些相对优势或竞争优势呢?而目前来说,面临的主要挑战又有哪些呢?

Francis:从企业的角度来说,只需更少的代码与更少的开发者就能够完成相同的工作,而且更少的代码库将更易于改动。只需很少的精力就能够达到优秀的应用性能(它并非绝对最快的语言,但非常接近于 Java 的性能,并且比起 Python 或 Ruby 快了一个数量级)。总的来说,Clojure 在以数据为中心(而不是以计算为中心),并且需求频繁改动的问题领域中能够发挥最大的功效。不过,寻找 Clojure 开发者比起 Java 等语言的开发者更困难,但你也无需大量的 Clojure 开发者。

不过,在许多问题领域中,代码的安全性或性能是至关重要的因素,并且需求相对比较确定。你可以选择某些结构式类型(Prismatic 的 Schema)或渐进代数式类型(Typed Clojure)的库,但这些库是可选的,或者还不够成熟。Clojure 也不适用于一些受限的环境,例如手机或平板电脑(有一些相关的替代方案可以用于小型客户端,例如 ClojureScript 或 Skummet)。

InfoQ:Clojure 在商业项目中得到了越来越多的应用,你认为带动了这一趋势最直接的因素是哪些?

Francis:许多应用如今都已转变为基于 web 的部署方式,他们需要进行横向扩展而不是纵向扩展,而不可变性从微观角度(线程级别的并发性)与宏观角度(集群级别的并发性)两方面大大地简化了横向扩展的过程。我认为这正是 Clojure 在商业项目上取得成功的一大因素。

另一个因素是 Clojure 托管于 JVM 环境中。一方面,人们在 Java 方面已经获得了丰富的经验,拥有大量的专家及软件,但他们又受挫于 Java 企业生态系统的复杂性。Clojure 让你能够逐步地迈向简单性的目标,而又无需完全抛弃过往的经验:它比 Java 更简单,表达能力也更强,但它也能够与 Java 共存。因此,对于那些不愿意完全受限于 Java,但又无法接受以截然不同的技术取代 Java 的风险的组织来说,Clojure 是一个良好的替代方案。

另一方面,来自动态语言背景的人往往是由于 Java 的静态类型以及复杂性方面的因素放弃了它,而 Clojure 对他们来说是一种可接受的语言,它同样是动态类型的语言,并且对于函数提供了一等支持,因此运行速度比本身的语言要快得多。当前最流行的动态语言实现都有着性能或并发性方面的限制(例如全局解释器的锁,又或者完全不支持线程!),而 JVM 并没有这方面的问题。Clojure 为这些开发者提供了一条出路,让他们能够利用 JVM 的强大特性,而又无需过多地触及 Java。

InfoQ:你对于 Clojure(语言)的发展有什么看法?

Francis:我认为 Clojure 开发者仍然受困于三个方面的问题,这或许将成为 Clojure 未来发展的动力。

首先,Clojure 运行时非常庞大,造成了启动的缓慢。对于那些在服务器环境中长时间运行的应用来说,这一点不成问题,但越来越多的人希望在运行时间较短的程序(例如命令行工具)或在一些资源受限的机器(例如 Android 手机)中使用 Clojure。Clojure 的极端动态特性此时就成了一个不利因素,因为它会降低应用的速度。Clojure 1.8 增加了一个可选的编译设置,名为“直接链接”,当某个 var 没有经过重定义时,它可消除 var 的间接特性。从而允许 Clojure 生成更小的 Java 类以及更多的静态调用。虽然它能够带来一些帮助,但仅有这一点还不够。此外,还有一些点子或许能够在未来的 Clojure 版本中出现,例如通过 tree-shaking(即删除无作用的代码)减小代码的体积,以及通过命名空间延迟加载的方式加快启动的速度。

其次,Clojure 的实现将出现在越来越多的主机上。ClojureCLR(.NET 平台上的 Clojure)出现的时间几乎与 Clojure 本身一样长,而 ClojureScript 在过去一年间的流行度得到了爆发式的增长。Clojure 曾经有一种以主机为中心的哲学,但越来越多的开发者希望编写的 Clojure 代码至少能够实现在 Clojure 与 ClojureScript 之间的相互移植(实际上,有许多 Clojure 库已经能够在不进行改动的情况下运行在这两个环境中了)。Clojure 1.7 引入了一个名为“reader conditionals”的特性,允许在 Clojure 实现中的同一个.cljc 文件中对代码段进行条件式评估。我希望开发者能够投入更多的精力,将不同版本的 Clojure 之间的平台区别保持在最低。比方说,Clojure 1.8 引入了额外的 string 函数,使开发者不必再调用某些 Java 方法,并提高了 Clojure 与 ClojureScript 代码之间的可移植性。

最后,人们对于代数类型(Algebraic Typing),尤其是渐进类型(Gradual Typing)产生了越来越多的兴趣。程序员希望编译器能够对类型及不变量进行检验,以减少他们必须编写的测试数量,同时又不希望被迫在整个程序中明确地定义类型。如我所说,在这方面已经展开了一些工作。在 Clojure 生态系统中已经出现了 Prismatic Schema、Herbert 和大量其他的运行时结构类型化库,而 Typed Clojure 项目还提供了可选的编译期渐进类型化的功能。我认为这些项目(尤其是 Typed Clojure)能够进一步成熟起来,并得到更多的应用。不过,我并不认为在 Clojure 核心功能中会为此进行任何改动。

读者可以选择购买本书的电子版或印刷版,本书的风格在可免费下载的两章中已得到了充分的展现,这两章分别是:Introducing Clojure,以及第 8 章,More on Functional Programming

关于作者

Francis Avila过去 8 年间一直在从事 web 方面的开发工作。在过去 4 年内,他在 Breeze 编写了大量专业的 Clojure 与 ClojureScript 代码,包括前端与后端代码。Breeze 是一家医学方面的小型软件公司,Clojure 是该公司唯一的开发语言。Francis 与他的妻子和两岁的女儿居住在路易斯安那州的拉斐特。

查看英文原文Clojure in Action, Second Edition, Review and Authors Q&A