Clojure 哲学

简单性、专心编程不受打扰(freedom to focus)、给力(empowerment)、一致性和明确性:Closure编程语言中几乎每个元素的设计思想都是为了促成这些目标的实现。javascript

学习一门新的编程语言每每须要花费大量的心思和精力,只有程序员认为他可以从他想学的语言中获得相应的回报,这种学习才是值得的。在使用面向对象技术对状态进行管理时,不管是因为面向对象技术内在的因素仍是别的偶然因素,都会带来许多没必要要的复杂问题,Clojure正是诞生于其建立者Rich Hickey对避免这些问题所作的种种努力。 因为Closure周到的设计方案基于的是在编程语言方面严谨的研究成果,并且在其设计过程当中对实用性有着强烈的愿景,因此,Clojure已经茁壮成长为一门重要的编程语言,它在当今编程语言设计领域扮演着一个无可置疑的重要角色。 从一方面讲,Clojure利用了软件事务内存(Software Transactional Memory,简称STM)、agent、在标示(identity)和数值类型(value type)之间划清界线、为所欲为的多态性(arbitrary polymorphism)以及函数编程(functional programming)等诸多手段,提供了一个有助于弄清楚整体状态的环境,特别是在面对并发机制时它更能发挥这方面的做用。从另一个方面讲,Clojure同Java虚拟机有着密切的关系,从而使得有望使用Clojure的开发者可以避免在利用现有的代码库时再为维护另一套不一样的基础设施付出额外的代价。php

在编程语言悠久的编年史中, Clojure 算是一个婴儿; 但它的一些俗语(简单的理解为 "最佳实践" 或者 "习惯用法") 源于拥有50年历史的Lisp语言和15年历史的Java语言。 (在吸取了Lisp 和 Java优秀传统的同时, 在许多方面,Clojure 也象征了一些直接挑战他们的变化。) 另外, 自从它问世以来就创建起来的充满热情的社区,已经发展出属于本身的独一无二的习惯用法集。一种语言的习惯用法有助于将比较复杂的表述 定义成简洁的呈现。 咱们确定会涉及到惯用的 Clojure 代码,可是咱们还会更深刻地讨论关于语言自己为何这样实现的缘由。html

在这篇文章中,咱们讨论关于现有编程语言中存在的一些不足,Clojure 正是用来解决这些不足的,在这些领域,它如何弥补了这些不足,以及Clojure体现出的许多设计原则。咱们还能够看到一些现有的编程语言对Clojure形成的影响。java

Clojure之道

让咱们慢慢开始吧。git

Clojure是一门执着于本身的见解的语言 —— 它并不想包含全部的范型(paradigm),也不想提供一项项的重点特性。相反,它只是以Clojure的方式,提供可以足以解决各类现实问题的全部特性。 为了从Clojure中得到最大的好处,你就应该带着和语言自己相同的愿景来写代码。在逐一讨论Clojure的语言特性时,咱们不只仅会给出每一个特性是作什么用的,并且还会讨论为何会有这样的特性以及特性最好的使用方式是什么。程序员

但在进行这些讨论以前,咱们先从一个比较高的层层看看Clojure背后最重要一些理念。图1列出的是Rich Hickey在设计Clojure时内心所想的整体目标以及Clojure所包含的可以支持这些目标得以实现的设计决策。sql

Clojure philosophy
图1: Clojure的整体目标,给出了Clojure背后的理念中所包含的一些概念以及它们之间的交叉关系。数据库

如图所示,Clojure的整体目标是由若干相互支撑的目标和功能组成的,在下面的几个小节中咱们将对它们进行一一讨论。express

简单性

要给复杂的问题写出简单的答案可不容易。但每一个有经验的程序员都曾遇到过将事情搞到没有必要的那种更加复杂程度的状况,为了完成手头的任务,必不可少要处理一些复杂状况,但前面说的这种没有必要的复杂状况与之不一样,能够将其称为次生复杂性(incidental complexity)(这个概念的详细状况能够参见"Out of the Tar Pit" )。Clojure致力于在不增长次生复杂性的前提下就可让你解决涉及大范围的数据需求(data requirement)、多并发线程以及相互独立开发的代码库等等方面的复杂问题。它还提供了一些工具,能够用来减小乍看起来象是具备必不可少的复杂性的问题。最终的特性集看起来并不老是那么简单,特别是在你还不熟悉它们的时候更是如此,但咱们认为,你慢慢就会发现Clojure可以帮你剥离多少复杂性。编程

举个次生复杂性的例子,现代的面向对象的语言趋向于要求每一段可执行的代码都要以类定义的层次、继承和类型定义的形式进行打包。 Clojure经过对纯函数(pure function)的支持摒弃了这一套陈规。纯函数只接受一些参数而且会仅仅基于这些参数产生一个返回值。 大量的Clojure程序都是由这样的函数构成的,并且绝大多数应用程序均可以作成这样式的,也就是说,在试图解决手头的问题时,须要考虑的因素会比较少。

不受打扰专心编程(Freedom to Focus)

编写代码的过程每每就是一个不断同使人分心的东西作斗争的过程,每当一种语言迫使你不得不考虑语法、操做符的优先级或者继承关系的层次结构时,它都是在添乱。Clojure努力让这一切保持到最简单的程度,从而不会称为你的绊脚石,不会在你象要探索一个基本想法时还须要进行先编译再运行的这种循环动做。它还向你提供了一些用于改造Closure自己的工具,这样你就能够创造出最适合与你的问题域(problem domain)的词汇和语法了 —— Clojure属于表示性(expressive)语言。它很是强大,可以让你以很是简洁的方式完成高度复杂的任务,同时还不会损失其可理解性。

可以实现这种不受打扰专心编程的一个关键在于格守动态系统(dynamic system)的信条。在Clojure程序中定义的几乎全部东西均可以再次进行从新定义,即便是在程序运行时也没有问题:函数、多重方法(multimethod)、类型以及类型的层次结构甚至Java的方法实现均可以进行从新定义。虽然在生产环境中(a production system)让程序一边运行一边进行重定义可能显得有点可怕,但这么作却在编写程序方面为你打开了一个能够实现各类使人惊叹的可能性的世界。用它能够对不熟悉的API进行更多的实验和探索,并为之增添一丝乐趣,而相比之下,这些实验和探索有时会掣肘于更加静态化的语言以及长时间的编译周期。

可是,Clojure可不只仅是用来寻找乐趣的。其中的乐趣只是Clojure在赋予程序员之前不敢想象的更高的编程效率时,所带来的副产品而已。

给力(Empowerment)

有些编程语言之因此会诞生,要么只是为了展现学术界所珍视的某些研究成果,要么就是用来探索某些计算理论的。Clojure可不属于这类语言。Rich Hickey曾在不少场合下说过,Clojure 的价值在于,你用Clojure能够编写出有意思并且也颇有用的应用程序。

为了实现该目标,Clojure力求实用 —— 它要成为可以帮助人们完成任务的一个工具。在Clojure的设计过程当中,要是须要在一个实用的方案和一个灵巧、花哨或者是基于纯理论的解决方案之间作出权衡选择时,每每实用方案都会胜出。Clojure本能够在程序员和代码库间插入一个无所不包的API,从而将程序员同Java隔离开来,但这么作的话,若是想用第三方Java库就会至关不便。所以,Clojure反其道而行之:它能够直接编译为同普通Java类以及方法彻底相同的字节码,中间无需任何封装形式。Clojure中的字符串就是Java字符串;Clojure的函数调用就是Java的方法调用;这一切都是那么简单、直接和实用。

让Clojure直接使用Java虚拟机就是这种实用性方面一个很是明显的例子。JVM在技术方面存在一些不足之处,好比在启动时间、内存使用、缺少尾递归调用优化技术(tail-call optimization,简称TCO)等等方面。但它仍不失为一种很是可行的平台 —— 它成熟、快速并且已获得普遍的部署。JVM支持大量不一样的硬件平台以及操做系统,它拥有数量惊人的代码库以及辅助工具。正是因为Closure这个以实用为上的设计决策使得,全部这一切Clojure均可以直接加以利用。

Closure采用了直接方法调用、proxy、gen-class、gen-interface、reify、definterface、 deftype以及defrecord等等手段,都是致力于提供大量的实现互操做性的选项,其实都是为了帮你完成手头的任务。虽然实用性对Clojure来讲很是重要,可是许多其它的语言也很实用。下文你将经过查看Clojure是如何避免添乱的,从而领会Clojure是如何真正成为一门鹤立鸡群的语言的。

明确性(Clarity)

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. — Dr. Seuss (译者注:这是一段英文绕口令,大体意思是:甲壳虫和甲壳虫在一个水坑里噼里啪啦打了起来,并且这个水坑仍是个瓶子里的水坑,因此他们就把这种装了滋滋乱叫的甲壳虫的瓶子叫作噼里啪啦乱做一团的水坑瓶。。。)

请看下面这段能够说是很是简单的一段相似Python的代码:

x=[5]
process(x)
x[0]=x[0]+1

这段代码执行结束后,x的值是多少?若是你process并不会修改x的值的话,就应该是[6],对吧?但是,你怎么能作出这样的假设呢?在不许确乱叫process作了什么以及它还调用了哪些函数的状况下,你根本就没法肯定x的值究竟是多少。

即便你能够确信process不会改变x的值,加上多线程后你要考虑的因素就又多了一重。要是另一个线程在第一行和第三行代码之间对x的值进行了改变会出现什么状况?让状况变得更加糟糕的还有,要是在第三行的赋值操做过程当中有线程对x的值进行设定的话,你能确保你所在的平台可以保证修改x的操做的原子性吗?要么x的值最终会是多个写入操做形成了乱数据?为了搞清除全部状况,咱们能够不断继续这种思惟练习,但最终结果毫无二致 —— 最后你根本就搞不清楚全部状况,最终结果却偏偏相反:乱做一团。

Clojure致力于保持代码的明确性,它提供了能够用来避免多种混乱状况的工具。对于上一段所述的问题,Clojure提供了不可变的局部变量(immutable local)以及持久性的集合数据类型(persistent collection),这两者能够一劳永逸地排除绝大多数由单线程和多线程所引发各类问题。

若是你所使用的语言在一个语法结构中混合了多种不相关的行为,你就会发现你还会深陷于其它几种混乱之中。Clojure经过对关注点分离(separation of concerns)保持警戒来消灭这些混。当代码开始变得零零散散时,Clojure能够帮你理清思路,当且仅当对手头的问题很是合适的状况下,可让你从新将它们结合起来。一般在其它一些语言中是混在一块儿的概念,Closure却对它们进行了分离处理,表1对这些概念作了一个对比。

混为一谈 分离开来
将对象同可变域(mutable field)混在了一块儿 对值(value)标示(identity)进行了区分
把类看成方法(method)的命名空间(namespace) 对函数的命名空间类型的命名空间进行了区分
继承关系层次结构是由类构成的 对名称的层次结构数据和函数进行了区分
在词法上将数据和方法绑定到了一块儿 对数据对象函数进行了区分
方法的实现嵌入到了整个类的继承关系链中 隔离了接口定义函数实现。

表1:Clojure中的关注点分离(Separation of concerns)。

有时候在咱们的思惟中很难将这些概念剥离开来,可是,一旦剥离成功,便可以带来无与伦比的明确性、充满力量的感受以及极大的灵活性,会让你绝对为之付出的一切都是值得的。有这么多不一样的概念可用以后,很重要的一点是,你的代码和数据要可以以一种始终一致的方式反映出这种变化。

一致性

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

语法的一致性指的是相关概念间在形式上的类似性。 在这方面有一个很是简单但颇具说服力的例子,for和doseq这两个宏具备相同的语法。它们俩所作的事情不同 —— for会返回一个lazy seq,而doseq是用来产生反作用的 —— 但它俩都支持彻底相同的内嵌循环、结构重组(destructuring)以及:when和:while控制条件(guard)。比较一下下面这个例子,一眼就能看出二者间的类似性了:

(for [x [:a :b], y (range 5) :when (odd? y)] [x y]) ;=> ([:a 1] [:a 3] [:b 1] [:b 3]) (doseq [x [:a :b], y (range 5) :when (odd? y)] (prn x y)) ; :a 1 ; :a 3 ; :b 1 ; :b 3 ;=> nil

这种类似性的价值在于,在两种状况下却仅需学习一个基本语法便可,并且若是须要的话,在这两种状况间进行互换也很是容易,好比将for换为doseq,或反之。

一样的,数据结构的一致性是对Clojure中全部持久性集合数据类型(persistent collection types)的一种刻意的设计,这种设计所提供的接口会尽量的互相保持必定的类似性,也就是使得它们的用途能尽量的普遍。这实际上就是对经典的Lisp哲学“代码既数据”一种扩充。Clojure的数据结构不只仅是用来保存大量的应用程序数据的,并且仍是用来保存应用程序自己的表达式元素(expression element)的。它们用于描述结构重组的形式(destructuring form),并为各类不一样种类的内置函数提供一种名称选项。在其它面向对象编程语言中,有可能会鼓励应用程序为保存不一样类型的应用程序数据而定义多种互不兼容的类,但Clojure会鼓励使用相互兼容的影射集map-like类的对象。

数据结构一致性带来的好处在于,为使用Clojure数据结构而设计的同一套函数能够适用于如下全部哲学场合:大规模数据存储、应用程序代码以及应用程序数据对象。你能够使用into构建前面所说的任意类型的数据,使用seq获得一个lazy seq并对其进行遍历,使用filter从它们当中挑选出符合某个特定断言(predicate)的全部元素等等等等。一旦你慢慢适应了Clojure中方方面面的这些函数丰富的功能,你再用Java或者C++应用程序中的Person类或者Address类时就会感到很是的憋屈。

简单性、专心编程不受打扰(freedom to focus)、给力(empowerment)、一致性和明确性。

Closure编程语言中几乎每个元素的设计思想都是为了促成这些目标的实现。在编写Clojure代码时,若是你能记住要尽其所能的简单化、给力以及不受打扰专心编程来解决手头真正的问题,那咱们就会认为,你将可以发现Clojure为你提供了可以让你走向成功所需的工具。

为何又弄了一种(新的)Lisp方言?

By relieving the brain of all unnecessary work, a good notation sets it free to concentrate on more advanced problems. — Alfred North Whitehead

一套良好的标示方法可将大脑从全部的琐碎中解脱出来,从而可以更加专一地解决更为高级的问题 - Alfred North Whitehead

随便到某个开源项目托管网站搜一下"Lisp interpreter(Lisp解释器)",就这么个不起眼的词,获得的搜索结果可能会多得让你数也数不清。实际上讲,计算机科学发展史中处处都散落着各类被人丢弃的Lisp实现方案。好心好意实现出来的各类Lisp来了又去,一路走来博得各类嘲笑,可要是明天你再搜一次的话,搜到的结果仍旧在漫无边际地与日俱增。既然知道这种传统如此残忍,为何还会有人愿意基于Lisp模型开发崭新的编程语言呢?

美感

在计算机科学发展史中,有一些很是聪明的人都对Lisp很是着迷。可是仅凭权威所说仍是不够的,你不能光凭这一点来对Lisp下结论。Lisp家族中的各类语言的真正价值可以直接从使用它们编写应用程序的行为中观察到。List的风格是一种极具表达力和很是感染人的风格,并且在不少状况下都具备一种全方位的美。快乐就在前方静静等待着Lisp新手。John McCarthy在他那篇开天辟地的文章"Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I(符号表达式的递归函数以及其机器计算)"中给Lisp所下的原始定义中仅用了7个函数和2种特殊形式(form)就定义出了整个一个语言:atom,car,cdr,cond,cons,eq,quote,lambda以及label.

经过对这9种形式进行组合,McCarthy就可以以一种可以让你屏息凝神的惊人方式描述出全部形式的计算方式。计算机程序员永生都在寻找美,而美多半都是以一种很是简单的形式出现。7个函数和2种特殊形式。还能有比这更美的吗?

极度灵活

为何Lisp可以坚持50多年而其它无数的语言却来也匆匆去也匆匆?其中缘由可能很复杂,但可能主要是由于做为一种语言的基因,Lisp能够孕育出极度灵活的语言。Lisp中哪哪都是括号和前缀表示法,这有时可能会让刚刚接触Lisp的人感到发怵,这些特色同其它非Lisp类型的编程语言大不相同。这种整齐划一的作法不只减小了须要记忆的语法规则的数量,并且还能使宏的编写变为小菜一碟。下面咱们看一个例子,这个例子咱们还会接着使用:

(defn query [max]
  (SELECT [a b c]
    (FROM X (LEFT-JOIN Y :ON (= X.a Y.b))) (WHERE (AND (< a 5) (< b ~max)))))

由于这篇文章不是用来说解SQL语句的,因此咱们但愿例子中的这些单词你并不会感到陌生。不管如何,咱们想表达的意思是,Clojure并无内置对SQL的支持。SELECT、FROM等等这些词可不是内置的form。它们也不是普通的函数,由于,若是SELECT是内置的或者是普通函数的话,a、b和c的用法就是错误的,由于它们尚未定义。

那么,怎样才能用Clojure定义出相似这样的领域特定语言(domain-specific language,简称DSL)呢?虽然这些代码还不能用于生产环境,也没有同任何真正的数据库服务器进行链接,但只须要列表1这种所示的一个宏和3个函数,前面的哪一个查询就可以返回相应的值:

(query 5)
;=> ["SELECT a, b, c FROM X LEFT JOIN Y ON (X.a = Y.b) WHERE ((a < 5) AND (b < ?))" [5]]

要注意的是,相似FROM和ON的这些词直接来自输入的表达式,而其它的相似~max和AND的这类词还须要通过特殊处理。在执行该查询时得到了5这个值的max是提取自SQL字符串字面量(literal),它由一个单独的向量(vector)提供。这是一种用来防止SQL注入攻击的比较完美的作法。AND这个form是从Clojure的前缀表示法(prefix notation)转化为SQL所需的中缀表示法(infix notation)转换后获得的。

列表1:定义一种领域特定语言将SQL嵌入到Clojure之中

(ns joy.sql
  (:use [clojure.string :as str :only []]) (defn expand-expr [expr] (if (coll? expr) (if (= (first expr) 'unquote) "?" (let [[op & args] expr] (str "(" (str/join (str " " op " ") (map expand-expr args)) ")"))) expr)) (declare expand-clause) (def clause-map {'SELECT (fn [fields & clauses] (apply str "SELECT " (str/join ", " fields) (map expand-clause clauses))) 'FROM (fn [table & joins] (apply str " FROM " table (map expand-clause joins))) 'LEFT-JOIN (fn [table on expr] (str " LEFT JOIN " table " ON " (expand-expr expr))) 'WHERE (fn [expr] (str " WHERE " (expand-expr expr)))}) (defn expand-clause [[op & args]] (apply (clause-map op) args)) (defmacro SELECT [& args] [(expand-clause (cons 'SELECT args)) (vec (for [n (tree-seq coll? seq args) :when (and (coll? n) (= (first n) 'unquote))] (second n)))])

在列表1中的第2行中,咱们使用了core中的字符串函数。在第6行中,我对不安全的字面量进行了相应的处理。在第9到11行,咱们将前缀表示法转换为中缀表示法。从第13行到15行,咱们对各类子句提供了支持。在第28行,咱们调用了适当的转换器。从第31行开始,咱们提供做为主要入口(entrypoint)的宏。

可是这里不是要说这个SQL DSL特别好 —— 这里有个更加完整的实现。 咱们想要说是,一旦你掌握了轻松建立相似这样的DSL的技能,你就会找出恰当的机会,定义比SQL适用面要窄的仅适用于解决你手头的应用程序中的问题的DSL。无论它是针对很是规的不支持SQL的数据存储的查询语言,仍是用来表示数学中某种比较晦涩的函数,或者是笔者想象不到其它什么应用领域,在仍可以使用Clojure编程语言全部特性的前提下还具备将Clojure做为基础进行灵活的扩展,这无疑就是一种革新。

虽然咱们不该该过多的讨论前文中SQL DSL的实现的细枝末节,可是再让咱们稍微看看列表1,跟随咱们的步伐,对其实现的比较重要的几个方面做进一步探讨。

从下往上看,你首先会注意到主要入口(main entry point),SELECT宏。这个宏返回一个由两个元素组成的向量(vector)—— 第一个元素产生自对expand-clause的调用,调用返回的是通过转换后的查询字符串。第二个元素也是一个向量,它在输入中用~标出。~又叫作unquote。还应该看到其中ree-seq的用法,它很是简洁的从一个由一组值组成的树,也就是输入表达式,中抽取出了所需的数据项。

函数expand-clause拿到子句中的第一个词以后,就在clause-map中对其进行查询并调用适当的函数,实际完成从Clojure的s表达式(s-expression)到SQL字符串的转换。clause-map为SQL表达式的每个部分提供了相应的具体功能:插入逗号等SQL语法所要求的内容,在子句当中还有须要转换的子句时对expand-clause进行递归调用。其中还包含了WHERE子句,经过调用expand-expr这个函数,它将前缀表达式转换为SQL所需的中缀形式。

总的来说,这个例子中所演示的Clojure的灵活性主要来自一个事实,那就是,Clojure中的宏能够接受代码form,就像这个SQL DSL的例子所示同样,它可以将代码看成数据进行处理 —— 对树进行遍历(walking tree)、对值进行转换等等。之因此可以这么作,不只仅由于咱们能够将代码看成数据对待,并且更是由于在Clojure程序中,代码就是数据。

代码即数据

“代码即数据”这个概念开始时比较难以理解。实现一种代码与其所包含的数据结构具备相同的基础的编程语言,是要以该语言自己基本的可塑性为前提的。当你的编程语言自己的表示形式就是其所固有的数据结构时,那么该语言自己具备了可以操纵它本身的结构和行为的能力。读完前面这句话,你的眼前可能会浮现出一个衔尾蛇(译者注:咬尾蛇是出现于古埃及宗教和神话中的图腾,其形象为一条正在吞食本身尾巴的蛇)的形象,要是这样也没有什么不对的地方,由于Lisp就是能够比做一个正在舔食本身的棒棒糖 —— 或者用一种更加正规的定义来讲,Lisp具备同像性(homoiconicity)。虽然Lisp的同像性须要你在思惟上有一个至关大的跳跃才能彻底掌握, 可是,咱们会逐步引导你完成这个掌握过程,但愿立刻你也能体会到它与生俱来的强大的力量。

函数式编程(Functional Programming)

立刻说出什么是函数式编程(functional programming)?回答错误!

千万别灰心 —— 其实咱们也不知道正确答案。函数式编程是计算领域中定义极其模糊的术语之一。 若是你向100个程序员询问他们对函数式编程的定义,你颇有可能会获得100个不一样的答案。固然,其中有些定义会比较类似,可是就和雪花同样, 天下没有两片雪花会是彻底如出一辙的。让状况变得更加糟糕的是,计算机科学领域中的大拿们每一个人的定义也经常同别人的定义相抵触。函数式编程各类定义的基本结构一样也不尽相同,就看你的答案是来自偏心使用Haskell, ML, Factor, Unlambda, Ruby, 仍是Qi来编写程序的人了。没有任何一我的、一本书或者一种语言能够称做是函数式编程的权威!因此,就同雪花虽多而不一样但它们基本上都是由水构成的同样,函数式编程的各类定义的核心总有一些万变不离其宗之处。

给函数式编程一个还说得过去的定义

对于函数式编程,不管你本身的定义是以lambda演算(lambda calculus)、单子I/O( monadic I/O)、委派(delegate)仍是java.lang.Runnable为中心,万变不离其宗的可能就算是某种形式 的过程(procedure)、函数(function)或者方法(method) ——  全部定义的根都在于此。函数式编程关注并致力于促进函数的应用以及整合。更进一步说,若是一门语言要想称为是函数式的编程语言,那么在该语言中,函数的概念必须具备首要地位(first-class),这个语言中的函数必须象该语言中的数据那样,也能够对函数进行存储和传递,还能够把函数看成返回值。函数式编程定义有无穷多个分支,这个核心概念是作不到无所不包,但好歹这能算是个讨论的基础。固然,咱们还会进一步给出Clojure风格的函数式编程的定义,其中要包括的有纯净度(purity)、不可变性(immutability)、递归(recursion)、惰性(laziness)以及 and 引用透明性(referential transparency)。

函数式编程的涵义

面向对象的程序员和函数式编程的程序员在看待问题和解决问题的方式上每每有很大的不一样。在面向对象的思惟方式中,每每会倾向于使用将问题域定义为一组名字(也即类)的方式来解决问题,而在函数式编程的思惟方式中,每每会将一系列动词(也即函数)的组合做为问题的解决方案。尽管这两类程序员颇有可能产生彻底等价的结果,但函数式编程的解决方案来得却更加简洁一些,可理解性和可重用性的程度也要更高一些。这口气可真大啊!咱们但愿你会赞成这个观点:函数式编程能够促进编程的优美程度。虽然要从名词性思惟转换为动词性思惟,须要你在整个观念上有一个大的转变, 这这个转变彻底是值得的。不管如何,咱们认为,只要你在这个问题上能有一个开放的心态,你就可以从Clojure身上学到不少彻底适用于你所选的语言中的知识。

为何说Clojure不怎么面向对象

Elegance and familiarity are orthogonal. — Rich Hickey
优雅同熟悉程度彻底无关 —— Rich Hickey

Clojure的诞生就是来自与很大程度上由并发编程的复杂性带来的挫败感,而面向对象编程的各类不足更加地加重了这种挫败感。本小节将对面向对象编程的这些不足之处进行探讨,为Clojure是函数式编程语言而不是面向对象的编程语言这个观点奠基一个基础。

术语定义

在咱们开始真正的探讨以前,先来定义一些有用的术语。(这些术语在Rich Hickey的演示稿 "Are We There Yet?(咱们是否已经到达了成功的彼岸?)"中也有相应的定义和细致的讲解)。

要定义的第一个最重要的术语就是时间。简单说来,时间指的就是在某个事件发生时的那个相对时刻。随着时间的推移,同一个实体(entity)相关联的全部属性 —— 不管是静态属性仍是动态属性,也不管是单一属性仍是组合属性 —— 都会慢慢造成一个综合体(concrescence),这个综合体能够认为就是该实体的标示(identity)。接着这个思路讲,在任何给定的时间点上,给实体的全部属性拍一个快照,就能够将其定义为该实体的状态(state)。 此概念下的状态属于不可变的状态,由于它在定义中并非随着实体自己的变化而发生变化,状态只是在整个时间中的某个给定时刻下实体的全部属性的一个体现。为了可以彻底理解这些术语,能够想象一下小孩玩的手翻书(译者注:指有多张连续动做漫画图片的小册子,因人类视觉暂留而感受图像动了起来。也可说是一种动画手法。—— 摘自wikipedia)。手翻书自己表明着标示。要想显示出画面总动画效果,你就要把手翻书中的另外一也图片翻过来。翻页的动做所以表明了手翻书中图片随着时间推移而造成的各个状态。停在某一个页后,看到的就是表明着那个时刻下的状态的图片。

还有一点很重要,须要注意:面向对象编程的教规并无对状态和标示划出清晰的界线。换句话说,这两个概念在面向对象的方法中被混淆为通常称作可变状态(mutable state)的这个概念了。在经典的面向对象模型之下,对象的属性能够彻底不受限制的发生改变,并不会刻意保留对象的全部历史状态。Clojure的实现方案企图在对象同时间相关的状态和标示之间作出一个严格的区分。咱们能够用前文提到的手翻书来讲明面向对象模型同Clojure的模型的不一样之处。可变状态之因此不一样, 由于在这种模型下用变化来对状态的改变进行建模就须要你置办一堆的橡皮。你的手翻书如今变成了只有一篇纸的书,为了对变化进行建模,你就必须将图片中须要变化的部分擦掉从新画出变化后的画面。你应该可以看到,这种可变性完全把时间的概念毁掉了,而状态和标示合也二为一了。

不可变性(immutability)是Clojure的理论基石,并且Clojure的实现方案一样也确保了对不可变性提供了高效地之处。经过关注于实现不可变性,Clojure彻底剔除了可变状态(这词使用了矛盾修辞法)这个概念,并且原先又对象所表明的东西如今都用值(value)来表明了。 从定义上讲,值(Value)指的是对象的恒定不变的表示性的数量(amount)、 大小(magnitude)或者时间点(epoch)。(有些实体并无表示性的值,Pi就是一个这样的例子,但在计算领域中,它应该是个无限小数,这事讨论起来可没完了。)你能够问问你本身:Clojure中的基于值的编程语义有着何种涵义?

经过严格遵循不可变性的模型,并发编程一会儿天然而然地变成了一个简单一点(虽然仍不简单)的问题了,也就是说,你不用担忧对象的状态会发生变化了,因此你就能够再也用不着为多个线程同时对同一个对象进行修改而担忧了,你爱在哪一个线程里使用哪一个对象就使用哪一个对象。Clojure还将值的改变同它的引用类型(reference types)进行了隔离处理。Clojure中的引用类型为标示提供了一层间接性(indirection),使用它能够得到具备一致性的状态,虽然有可能不老是最新的状态。

由命令“烤制而成”

命令式编程(Imperative programming)是当今占有主导地位的编程范型(paradigm)。命令式编程语义最纯正的定义莫过于它是用一系列的语句不断修改着程序的状态。在本文的撰写之时(并且恐怕在之后很长一段时间以内),命令式编程中最受偏心的就是面向对象的风格了。这件事自己并无什么很差的,毕竟采用面向对象的命令式编程技术得到成功的软件项目数不胜数。但从并发编程的角度来看,面向对象的命令式编程模型就有点自拆墙角了。它容许(甚至提倡)对变量(variables)的值不加丝毫限制的修改,因此命令式编程模型并不直接支持并发编程。由于它容许一种肆无忌惮的改变变量的值,因此它根本没法保证变量可以包含着你所指望的值。面向对象的编程方法还更甚一步,它状态合并到了对象内部。虽然个别的方法能够经过加上锁定机制(locking scheme)变为线程安全的(thread-safe)方法,要是不把多是很是复杂的锁定机制扩大到必定范围的话,就根本没法保证在多个方法调用时对象的状态仍然可以保持一致性。与此相反,Clojure强调函数式编程方法和不可变性,并在状态、时间和标示间作了相应的区分。可是也不能说面向对象编程方法失败了。实际上,这种方法也有不少方面对不少颇有用的编程实践有促进做用。

OOP所提供大部分的特性Clojure也具有

有一点应该说清除,咱们可不是想鄙视使用面向对象技术进行编程的程序员。相反,很重要的是咱们要找出面向对象编程(OOP)的缺点,藉此咱们才能提升咱们的技艺。在接下来的几个小部分中,咱们还要说说OOP的比较强大的地方,以及Clojure对这些OOP的强大之处是以什么样的方式直接或者有时是加以改进后采纳的。

多态(Polymorphism)指的是一个函数或方法具备这样的能力:它能够根据目标对象类型的不一样儿具备不一样的定义。Clojure经过多重方法(multimethod)和协议(protocol)这两者提供了对多态的支持,这两种机制比许多其它语言中的多态更加开放、更具可扩展性。

列表2:Clojure中的多态协议

(defprotocol Concatenatable
  (cat [this other]))
(extend-type String Concatenatable (cat [this other] (.concat this other))) (cat "House" " of Leaves") ;=> "House of Leaves"

在列表2中咱们定义了一个叫作Concatenatable的protocol(协议),这种协议能够将一个或多个方法(此例中只有一个方法,cat)组成一组并将这组方法定义为方法集(set)。这个定义意味着,函数cat将可适用于知足Concatenatable协议的任何对象。接着咱们还将该协议extend(扩展)到了String类,并给出了具体的实现 —— 也就是给出了一个函数体,将参数中的other字符串链接到了字符串this以后。咱们还能够将该协议扩展到其它的类型,好比:

(extend-type java.util.List
  Concatenatable (cat [this other] (concat this other))) (cat [1 2 3] [4 5 6]) ;=> (1 2 3 4 5 6)

如今这个协议就扩展到了两种不一样的类型中,String和java.util.List,这样一来,就能够将这两种类型中的任何一种类型的数据做为第一个参数对cat函数进行调用了 —— 不管是哪一种类型都会调用相应类型的函数实现。

应该指出的是,String是在咱们定义这个协议以前就已经定义好了(该例子中的String是由Java自己定义的),即便如此,咱们扔可以将该新协议扩展到String中。在许多其它的语言中这种作法是没法实现的。例如,Java要求,你只能在定义好全部的方法名称并将它们定义为一个小组(这个小组在Java里叫作interfaces)以后,你才能定义实现该interface的类,咱们将这种限制条件称为表示性问题(expression problem).

表示性问题指的是,对于已有的具体类(concrete class)和该具体类并无实现的一组已有的抽象方法而言,若是不对定义这两者的代码进行修改,就没法让该具体类实现这组抽象方法。在面向对象的语言中,你能够在一个你可以控制得了的具体类中实现以前定义的抽象方法(这种实现可称为接口继承),可是若是该具体类并不在你的控制范围以内,那么让它实现新的或者现有的抽象方法的可能性通常来讲会很是小。有些动态语言,好比Ruby和JavaScript,提供了该问题的部分解决方案,它们容许你为已有的具体对象添加新的方法,有时这种特性被称为猴子补丁法(monkey-patching).

只要你以为有意义,Clojure中的协议能够对任意的类型进行扩充,即便在被扩展类型原先的实现者或者要进行扩展的协议原先的设计者从未料到你要这么作,也彻底没有问题。

Clojure提供了一种子类型化(subtyping)的形式,这种子类型化能够用来建立临时性类型层次结构(ad-hoc hierarchy)。Clojure经过使用协议机制一样也提供了同Java中的接口相似的功能。将逻辑上能够分为一组的方法定义为一个方法集,你就能够开始为数据类型抽象机制定义它们必须遵循的各类协议(protocol)了。这种面向抽象机制的编程(abstraction-oriented programming)模型在构建大规模应用程序中的做用很是关键。

若是说Clojure不是面向类的,那么它是若是提供封装(encapsulation)功能的呢?假设你须要这么一个简单的函数, 它能够根据一种棋局的表示形式以及一个指示性的坐标返回一种对棋子在棋盘中的表示形式。为了让实现代码尽量的简单,咱们将使用一个vector,它包含了一组下面这个列表所示的表明着各类颜色的棋子的字符:

列表3:用Clojure表示出一个简单的棋盘。

 (ns joy.chess)

(defn initial-board []
  [\r \n \b \q \k \b \n \r
    \p \p \p \p \p \p \p \p
    \- \- \- \- \- \- \- \-
    \- \- \- \- \- \- \- \-
    \- \- \- \- \- \- \- \-
    \- \- \- \- \- \- \- \-
    \P \P \P \P \P \P \P \P
    \R \N \B \Q \K \B \N \R])

列表3中的第5行中用小写字母表示的是深色棋子,在第10行中用大写字母表示的是浅色棋子。 既然国际象棋已经够难下的了,咱们就不须要在棋局的表示方式上面太为难本身了。上面代码中的数据结构直接对应着如图2所示的国际象棋开局时棋盘的实际状况。

Clojure philosophy chessboard illustration
图2:代码对应的棋盘布局

从上图中能够看出,黑色的棋子是用小写字母来表示的,白色棋子用大写字母表示。这种数据结构可能不是最好的数据结构,但用它来做为讨论的继承还算不错。如今暂时你能够忽略实际的实现细节,先关注一下用于查询棋盘布局的客户端接口。此时此刻正是一个很是完美的机会,体会一下如何经过使用封装机制来避免让客户端过多的关心实现细节。幸运的是,支持闭包(closure)的编程语言可以将一组函数及其相关的支撑性数据组合起来使用,从而自动提供一种形式的封装机制。 (通常咱们把这种形式的封装机制称为模块模式(module pattern)。可是,JavaScript中所实现的模块模式还提供了必定级别的数据绑定机制,而在Clojure中却并不是如此。)

列表4中所定义的函数意图不言而喻(它们还有一个特别的好处,就是这些函数能够用来根据任意大小的一维表示方式投射出相应的二维结构,咱们将这部分留做练习共你使用)。咱们使用了defn-这个宏将相应的函数定义到了命名空间(namespace)joy.chess之中,该宏所建立的是该命名空间中的私有函数。在这种状况下,使用lookup函数的命令应该是(joy.chess/lookup (initial-board) "a1")这样的。

列表4:对棋盘的布局进行查询

(def *file-key* \a)
(def *rank-key* \0)

(defn- file-component [file]
  (- (int file) (int *file-key*))) (defn- rank-component [rank] (* 8 (- 8 (- (int rank) (int *rank-key*))))) (defn- index [file rank] (+ (file-component file) (rank-component rank))) (defn lookup [board pos] (let [[file rank] pos] (board (index file rank))))

在第4和第5行,咱们计算的是水平投射结果。在第7和第8行,计算出了垂直投射的结果。从第11行开始,咱们将一维的布局方式投射到了逻辑上是二维结构的棋盘之上。

在查看使用了习惯用法的Clojure的源代码时,你遇到的最多的封装形式就是使用命名空间进行封装了。可是,使用词法闭包(lexical closure)就能够提供更多可选的封装机制:块级(block-level)封装(如列表5所示)和局部封装(local encapsulation)。这两种方式均可以很是有效的将不重要的实现细节集合到一个更小的范围之中。

列表5: 使用块级封装机制

(letfn [(index [file rank]
          (let [f (- (int file) (int \a)) r (* 8 (- 8 (- (int rank) (int \0))))] (+ f r)))] (defn lookup [board pos] (let [[file rank] pos] (board (index file rank)))))

将相关的数据、函数和宏集合起来放到适合它们的最具体最小的范围之中每每都会是个好主意。虽然你仍然能够象之前那样调用lookup,可是它的那些辅助函数的可见范围就要小多了 —— 这个例子中它们仅在命名空间joy.chess中可见。在上文的代码中,咱们并无将file-component和rank-component这两个函数以及*file-key*和*rank-key*这两个值定义到命名空间之中,只是把它们包进了用在letfn宏的代码体中定义的块级index函数之中 。在这块代码中,咱们接着定义了lookup函数, 这样就可以把同实现细节相关的函数和form隐藏起来,起到了避免将棋盘API的过多细节暴露给客户端的做用。可是,咱们还能够象下个列表中所示的那样,对封装的范围作出进一步的限制,将其缩小到一个适当的函数的局部范围以内。

列表6:局部封装(Local encapsulation)

(defn lookup2 [board pos]
  (let [[file rank] (map int pos) [fc rc] (map int [\a \0]) f (- file fc) r (* 8 (- 8 (- rank rc))) index (+ f r)] (board index)))

在这最后一步中,咱们将全部同实现相关的具体细节塞进了lookup2这个函数的函数体之中。 这么作就将index函数和全部辅助性的值局部化到了同它们惟一相关的部分 —— lookup2之中。除此以外还有一个额外的好处,lookup2简明扼要而不又失可读性。 可是Clojure却回避了在绝大多数面向对象编程语言中颇具特点的数据隐藏式的封装机制(data-hiding encapsulation)。

并不是万物皆对象

最后还要说的是面向对象编程中另外的一个缺点,它将函数和数据捆绑得太紧密了。实际上,Java编程语言会强迫你必须彻底在它限制性很是强的"名词王国(Kingdom of Nouns)." 中所包含的方法中实现全部的功能,因此你的整个程序必须彻底构建于类的继承层次结构之上。这种环境的限制性太强了,致使程序员常常对大量方法和类的很是蹩脚的组织方式熟视无睹。也正是因为Java里严格的以对象为中心的视角无所不在,才致使Java代码写得每每都比较长,也比较复杂。Clojure中的函数也是数据,但这一点也没有给在数据和使用这些数据的函数间进行解藕形成任何不利的影响。程序员眼中的类大部分实际上是Clojure中经过映射表(map)和记录(record)提供的数据表。给万物皆对象这种观点最后一击的是有本说说的在数学家眼里几乎没有什么东西是对象(mathematicians view little (if anything) as objects). 数学反而是创建在一组组元素通过函数运算以后所造成的关系之上的。

结束语

在这篇文章中咱们讨论了大量的概念。若是你仍然搞不清Clojure是怎么回子事,也没有关系 —— 咱们明白,谁也不可能一会儿就掌握这么多的概念。要弄懂这些概念是须要花点时间的。对有函数式编程方面的背景知识的读者来讲,本讨论中的不少内容可能并不陌生,但在细节上会有让人意想不到的变化须要适应。但对于背景知识彻底来自于面向对象编程的读者来说,可能会感到Clojure同他们所熟悉的都想大相径庭。虽然在不少方面它的确不一样,但是Clojure真的可以很是优美地解决你平常工做中所碰到的问题。虽然Clojure是从不一样于传统的面向对象技术的角度来着手解决软件问题的, 可是它解决问题的方法也是在面向对象技术的优势和缺点激励下造成的。有了这个思想认识做为基础,咱们鼓励你再接再砺,对Clojure进行更进一步的探索。

Michael Fogus,软件开发人员,在分布式仿真、机器视觉和专家系统的建设方面经验丰富。他活跃于Clojure社区以及Scala社区。 Chris Houser为Clojure作出了突出的贡献,亲自实现了Clojure的若干特性。本文改编自他俩合著的书The Joy of Clojure: Thinking the Clojure Way《乐享Clojure:Clojure的思惟方式》.

相关文章
相关标签/搜索