簡單性、專心編程不受打擾(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
讓咱們慢慢開始吧。git
Clojure是一門執着於本身的見解的語言 —— 它並不想包含全部的範型(paradigm),也不想提供一項項的重點特性。相反,它只是以Clojure的方式,提供可以足以解決各類現實問題的全部特性。 爲了從Clojure中得到最大的好處,你就應該帶着和語言自己相同的願景來寫代碼。在逐一討論Clojure的語言特性時,咱們不只僅會給出每一個特性是作什麼用的,並且還會討論爲何會有這樣的特性以及特性最好的使用方式是什麼。程序員
但在進行這些討論以前,咱們先從一個比較高的層層看看Clojure背後最重要一些理念。圖1列出的是Rich Hickey在設計Clojure時內心所想的整體目標以及Clojure所包含的可以支持這些目標得以實現的設計決策。sql
圖1: Clojure的整體目標,給出了Clojure背後的理念中所包含的一些概念以及它們之間的交叉關係。數據庫
如圖所示,Clojure的整體目標是由若干相互支撐的目標和功能組成的,在下面的幾個小節中咱們將對它們進行一一討論。express
要給複雜的問題寫出簡單的答案可不容易。但每一個有經驗的程序員都曾遇到過將事情搞到沒有必要的那種更加複雜程度的狀況,爲了完成手頭的任務,必不可少要處理一些複雜狀況,但前面說的這種沒有必要的複雜狀況與之不一樣,能夠將其稱爲次生複雜性(incidental complexity)(這個概念的詳細狀況能夠參見"Out of the Tar Pit" )。Clojure致力於在不增長次生複雜性的前提下就可讓你解決涉及大範圍的數據需求(data requirement)、多併發線程以及相互獨立開發的代碼庫等等方面的複雜問題。它還提供了一些工具,能夠用來減小乍看起來象是具備必不可少的複雜性的問題。最終的特性集看起來並不老是那麼簡單,特別是在你還不熟悉它們的時候更是如此,但咱們認爲,你慢慢就會發現Clojure可以幫你剝離多少複雜性。編程
舉個次生複雜性的例子,現代的面向對象的語言趨向於要求每一段可執行的代碼都要以類定義的層次、繼承和類型定義的形式進行打包。 Clojure經過對純函數(pure function)的支持摒棄了這一套陳規。純函數只接受一些參數而且會僅僅基於這些參數產生一個返回值。 大量的Clojure程序都是由這樣的函數構成的,並且絕大多數應用程序均可以作成這樣式的,也就是說,在試圖解決手頭的問題時,須要考慮的因素會比較少。
編寫代碼的過程每每就是一個不斷同使人分心的東西作鬥爭的過程,每當一種語言迫使你不得不考慮語法、操做符的優先級或者繼承關係的層次結構時,它都是在添亂。Clojure努力讓這一切保持到最簡單的程度,從而不會稱爲你的絆腳石,不會在你象要探索一個基本想法時還須要進行先編譯再運行的這種循環動做。它還向你提供了一些用於改造Closure自己的工具,這樣你就能夠創造出最適合與你的問題域(problem domain)的詞彙和語法了 —— Clojure屬於表示性(expressive)語言。它很是強大,可以讓你以很是簡潔的方式完成高度複雜的任務,同時還不會損失其可理解性。
可以實現這種不受打擾專心編程的一個關鍵在於格守動態系統(dynamic system)的信條。在Clojure程序中定義的幾乎全部東西均可以再次進行從新定義,即便是在程序運行時也沒有問題:函數、多重方法(multimethod)、類型以及類型的層次結構甚至Java的方法實現均可以進行從新定義。雖然在生產環境中(a production system)讓程序一邊運行一邊進行重定義可能顯得有點可怕,但這麼作卻在編寫程序方面爲你打開了一個能夠實現各類使人驚歎的可能性的世界。用它能夠對不熟悉的API進行更多的實驗和探索,併爲之增添一絲樂趣,而相比之下,這些實驗和探索有時會掣肘於更加靜態化的語言以及長時間的編譯週期。
可是,Clojure可不只僅是用來尋找樂趣的。其中的樂趣只是Clojure在賦予程序員之前不敢想象的更高的編程效率時,所帶來的副產品而已。
有些編程語言之因此會誕生,要麼只是爲了展現學術界所珍視的某些研究成果,要麼就是用來探索某些計算理論的。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是如何真正成爲一門鶴立雞羣的語言的。
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爲你提供了可以讓你走向成功所需的工具。
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)?回答錯誤!
千萬別灰心 —— 其實咱們也不知道正確答案。函數式編程是計算領域中定義極其模糊的術語之一。 若是你向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身上學到不少徹底適用於你所選的語言中的知識。
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)的缺點,藉此咱們才能提升咱們的技藝。在接下來的幾個小部分中,咱們還要說說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所示的國際象棋開局時棋盤的實際狀況。
圖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的思惟方式》.