Clojure 開發那些事

jiacai2050發表於2016-12-31

Clojure —— 新世紀的 Lisp 方言,相信大多數同學多多少少都聽過,畢竟有個殺手級應用 Storm,但是真正去寫 Clojure 的同學估計不多,國內也罕見哪個公司招 Clojure 程式設計師。
作為推廣 Clojure 萬里長城的第一步,這篇文章首先介紹為什麼要使用 Lisp 開發,之後開始介紹 Clojure 語法入門,緊接著介紹 Clojure 開發環境搭建,然後介紹使用第三方庫時的一些注意點,最後介紹一下常見的測試方法。本篇文章所介紹內容都是我自己實踐得出,不足之處請各位 Clojurians 指出。

Why Lisp

Lisp 語言誕生這麼久了,為什麼一直那麼小眾?原因就在於 Lisp 語言過於強大,不必也不可能像 Java 那麼普及。你能要求每個人都能為 CTO 嗎?
矽谷創業之父 Paul Graham 在其著作《黑客與畫家》中極力推薦 Lisp 語言,並且講到很多 Lisp 的特性逐漸融入到其他語言中。該書中列舉了 Lisp 中 9 種新思想,依次為:

  1. 條件結構 if-then-else
  2. 函式也是一種資料型別
  3. 遞迴
  4. 變數的動態型別,所有變數都是指標
  5. 垃圾回收機制
  6. 程式由表示式組成
  7. 符號型別,符合實際是一種指標,指向儲存在雜湊表中的字串
  8. 程式碼使用符號和常量組成的樹形表示法
  9. 無論什麼時刻,整個語言都是可用。Lisp 並不真正區分讀取期、編譯期和執行期

前 7 種特性可以在如今較流行的程式語言找到,但最後兩種是 Lisp 特有的。Lisp 最擅長的領域是寫編輯器(超程式設計)、領域特定語言DSL,現在用的最廣的是 Emacs 與 AutoACD,其對應的指令碼語言分別是 Emacs LispAutoLisp
可以好不誇張的說,軟體也複雜,越適合用 Lisp。其次,Clojure 作為新世紀的 Lisp 方言,在 Web 、大資料、資料庫等現在常見領域都有豐富的類庫與文件。

目前國內使用 Clojure 成功案例較少,LeanCloud 在其招聘網頁上寫到其成員都是 Clojure 社群成員,但並不瞭解其內部使用情況。國外的就比較多了,可以參考 Clojure 之父 Rich Hickey 所在公司 Congitect 列舉的 Success Stories

語法入門

括號

Lisp 語法最顯著的特點是“括號多”,不過這只是其外在表現,內在表現是閱讀程式碼的方式,需要從最裡面的表示式開始,比如:

;; Clojure
> (split (upper-case "hello, world") #", ")
["HELLO" "WORLD"]

;; Python
>>> "hello, world".upper().split(", ")
['HELLO', 'WORLD']複製程式碼

為了防止過度巢狀,需要經常定義一些輔助函式,很幸運,Clojure 裡面函式是一級成員,這意味著函式可以作為引數傳入,也可以作為函式值返回,能夠進行這兩類操作的函式稱為“高階函式”(high-order functions),這在任何一門函式式語言中都很普及。

除了最基本的圓括號()外,方括號[]與花括號{}在 Clojure 用的也比其他 Lisp 方言中多。

[1 2 "buckle my shoe"]        ;; 陣列
{:ace 1, :deuce 2, "trey" 3}  ;; 雜湊表
#{:a :b :c}                   ;; 集合複製程式碼

Clojure 中基本的資料結構可以參考其官方網站,我個人覺得,Lisp 方言的英文介紹往往過於精煉、晦澀,不適合初學者直接閱讀,為了夯實基礎,還是建議大家找本書來看,看書的好處是不僅僅知道某個知識點,更重要的是瞭解不同知識點之間的區別與聯絡,初學期間,我閱讀了下面兩本書:

  • The Joy of Clojure,這本書對我幫助比較大,但是網上普遍說這本書比較難懂,我只能說蘿蔔青菜各有所愛。
  • Clojure程式設計,這本書應該毋庸置疑是新手的必須書

除了看書外,下面的文件也非常 newbie-friendly,推薦大家多去逛逛:

資料不可變

括號問題適應後,另一個比較挑戰的是資料的不可變性,這融合在 Clojure 語言的設計之中,表象就是沒有賦值語句了,但在實現時,為了達到時間、空間上的高效,採用了非常複雜的演算法,我到現在也還是一知半解,不是很清楚。《The Joy of Clojure》一書中有簡單介紹,不過我覺得初學者可以完全不用去關心實現的細節,在遇到效能問題時在考慮去優化。我這裡放一些相關的資料,有興趣的讀者可以自取:

寄宿性語言

Clojure is desgined to be a hosted language.

這一點非常重要,估計也是為什麼 Clojure 較其他 Lisp 方言更流行的原因。Clojure 的宿主語言現在主要有兩個:一個是最多最多的的基於 JVM 的;另一個是基於微軟 .NET 的 Clojure-CLR,現在還有一個發展迅猛的 ClojureScript,可以將 Clojure 程式碼編譯為無處不在的 Javascript。

這也就意味我們或多或少需要了解這些宿主語言,比如 Clojure 裡面沒有提供直接操作檔案系統、網路的類庫,而是採用間接的方法去呼叫其宿主語言的相應類庫。這一點也讓 Clojure 在生產環境中使用變得可能,比如 clj-http 就是對 Apache HttpComponents 的包裝,更符合 Clojure 使用習慣而已。

開發環境搭建

工欲善其事,必先利其器。
這裡主要介紹 Intellj + Cursive 與 Emacs + Cider 兩個環境,這兩個是我用的最順手,也是現在較為流行的方式。

Intellj + Cursive

在上面語法入門部分就介紹了,Clojure 與宿主語言經常需要互動,毋庸置疑 Intellj 是 Java 開發的利器,社群版足以滿足需要,不用再去做找破解版那些不道德的事情,如果你還在用 Eclipse,可以考慮遷移了。
Cursive 做到了開箱即用,而且足夠的好用,而且也有非商業免費版,這極大方便了學生黨,適應了中國國情。

安裝、使用比較簡單,通過 Intellj 外掛管理器安裝後,設定下快捷鍵型別就可以使用了。

Clojure 開發那些事
Cursive 快捷鍵設定

Clojure 開發那些事
Cursive REPL

Emacs + Cider

作為一門 Lisp 方言,怎麼能沒有一個好的 Emacs mode 呢?Cider 全稱

The Clojure Interactive Development Environment that Rocks for Emacs

而且 Emacs 本身就是個用 Lisp 方言寫的“作業系統”,對以括號著稱的 Lisp 語言有天然的支援,括號匹配主要是 Paredit mode,可以方便的把括號作為一個整體操作,不過像 Cursive 這種外掛也整合了 Paredit 的主要功能,所以不用 Emacs 的同學也不用擔心,畢竟 Emacs 學習成本實在是太高,我個人覺得比 Vim 有過之而無不及,相對於 Vim 的模態概念,Emacs 裡面通過 Ctrl 與 Meta 鍵來與一般按鍵區別,這裡我們不必對某個編輯器有過多的偏見,它們都是生產力的工具而已,寫好程式碼才是重要的。

初學者如果要嘗試 Emacs 建議參考《Clojure For the Brave and True》的第二章How to Use Emacs, an Excellent Clojure Editor,我最初的環境也是仿照這份配置,然後一點點根據自己的需求增加的。 .emacs.d 是我的配置,供大家參考。

Clojure 開發那些事
Emacs + Cider 程式設計環境

Emacs + Cider 的組合相比 Intellj + Cursive 最大的優勢就是對巨集的支援,Cider 提供了對巨集展開的快捷鍵,但在 Cursive 中我沒找到,不過巨集也是比較高階的功能,初學者應該用不到,等到用的多的時候,就可以把 Emacs 環境熟悉起來了。

最後還是建議初學者不要用 Emacs,學習成本太大,而且很容易就把注意力轉移到編輯器的學習上,等到學習了一段時間後在嘗試不遲。

第三方類庫的選擇

由於 Clojure 語言定位就是個寄宿語言,所以無論是 Web 框架,還是資料庫連線池,Clojure 裡都有與 Java 版相對應包裝類庫,大家不必擔心要使用某個功能,而沒有相應庫的問題,但是這裡我必須說明一點,Clojure 類庫的文件對初學者不夠友好,最起碼對我來說是的,我相信我不可能是個例。就拿列印日誌來說,Github 上搜一下,應該能夠找到最 idiomatic 應該是 timbre,通讀其 README 後,怎麼配置還不是很清楚,繼續 Google,找到

這時我才能夠知道怎麼去定製他的appenders等各種引數,也可能是我個人的理解能力比較差,不過這裡介紹一個非常實用並且適用於所有語言的方法,那就是看這個專案的test,test 裡面核心的功能肯定會涉及到,然後照貓畫虎就可以了。

其實,在使用第三方類庫之餘,多去了解其實現,程式碼從 Github 上 Clone 下來,慢慢看,Clojure 裡面提供了很多實用的小方法,像partition, juxt, group-by等等不一而足,最好帶著 issue 裡面的問題去看程式碼,說不定你就從使用者變成了開發者呢,我第一個嘗試給了 http-clj

除錯 debug

程式碼一次寫對的機率基本為0,掌握一定的測試技能是每個同學的基本功,下面簡單介紹下 Clojure裡面常用的除錯方法。

println

(let [headers         (:headers ring-request)
      header-names    (keys headers)
      ;; The following underscore is a convention for "unused variable"
      _               (println "Headers:" header-names)  ;; <-- this
      header-keywords (map keyword header-names)]
;; etc
)複製程式碼

最簡單實用的 println,但問題是我們需要把要監控的變數打兩次,這在變數比較多的時候比較麻煩,可以採用下面的 spyscope

spyscope

Spyscope 庫可以解決上println的問題,他提供三個reader tags來監控變數,用法極為簡單:

(let [headers         (:headers ring-request)
      header-names    #spy/p (keys headers)       ;; <-- print out what header-names is
      header-keywords (map keyword header-names)]
;; etc
)複製程式碼

tools.trace

上面介紹的方法都需要修改原始碼,有沒有不用修改的呢?答案是肯定的,clojure.tools.trace,Github 上的 README 比較詳細,大家可以可以去了解,我目前在自己的專案裡面還沒有采用過這個方法。

Intellj Debug

藉助於 IDE 的優勢,我們可以打斷點,一步一步除錯,但是 Cursive 對巨集的支援比較有限,目前出來把巨集展開外,沒找到好的除錯巨集的好方法。

Clojure 開發那些事
Intellj debug

nrepl

Clojure 的 REPL 可以連線到遠端伺服器上的程式中,直接對程式中的函式或變數進行修改,這是非常便利的,對於很多執行時的錯誤可以採用這種方式解決,Emacs 與 Intellj 裡面都提供了連線遠端 REPL server 的方式。

Clojure 開發那些事
Intellj 連線遠端 REPL

Emacs 裡面是:M-x cider-connect
lein 裡面是:lein repl :connect 192.168.50.101:4343

總結

Clojure 在國內算是非常非常小眾,介紹 Clojure 開發的文章也比較少,僅有的也只是一些簡單的語法介紹或者概念闡述。
我從讀完 SICP 後就一直想把 Lisp 作為我的主力語言,正好趁著這次機會,希望能夠彌補國內 Clojure 文件較匱乏的情況,之後我會陸陸續續把自己使用 Clojure 開發的經歷分享出來,供後來的 Clojurians 參考,這也算是 17 年的第一個小目標吧。

Clojure 開發那些事
關於 Clojure 的 RSS 收集

上面是我目前收集關於 Clojure 的 RSS,大家可以根據標題去搜尋,熱愛 Clojure ,從不做伸手黨開始。?

擴充套件閱讀

Clojure 開發那些事
KeepWritingCodes 微信公眾號

PS: 微信公眾號,頭條,掘金等平臺均有我文章的分享,但我的文章會隨著我理解的加深不定期更新,建議大家最好去我的 部落格 liujiacai.net 閱讀最新版。

相關文章