Clojure入门教程: Clojure – Functional Programming for the JVM中文版
http://xumingming.sinaapp.com/302/clojure-functional-programming-for-the-jvm-clojure-tutorial/
api:http://richhickey.github.com/clojure/api-index.html
Clojure入門教程: Clojure – Functional Programming for the JVM中文版
2011 年 12 月 07 日 xumingming 作者: xumingming | 可以轉載, 但必須以超鏈接形式標明文章原始出處和作者信息及版權聲明網址: http://xumingming.sinaapp.com/302/clojure-functional-programming-for-the-jvm-clojure-tutorial/
?
本文翻譯自:Clojure – Functional Programming for the JVM 轉載請注明出處
內容列表
| 簡介 | 條件處理 | 引用類型 |
| 函數式編程 | 迭代 | 編譯 |
| Clojure概述 | 遞歸 | 自動化測試 |
| 開始吧 | 謂詞 | 編輯器和IDE |
| Clojure語法 | 序列 | 桌面程序 |
| REPL | 輸入輸出 | Web應用 |
| Bindings | 解構 | 數據庫 |
| 集合 | 名字空間 | 類庫 |
| StructMaps | 元數據 | 結論 |
| 定義函數 | 宏 | 引用 |
| 和Java的互操作 | 并發 | ? |
簡介
這篇文章的目的是以通俗易懂的方式引導大家進入Clojure的世界。文章涵蓋了cojure的大量的特性, 對每一個特性的介紹我力求簡介。你不用一條一條往下看,盡管跳到你感興趣的條目。
請把你的意見,建議發送到mark@ociweb.com(如果是對文章翻譯的建議,請直接在文章下面留言: http://xumingming.sinaapp.com/302/clojure-tutorial/)。我對下面這樣的建議特別感興趣:
- 你說是X, 其實是Y
- 你說是X, 但其實說Y會更貼切
- 你沒有提到X, 但是我認為X是一個非常重要的話題
對這篇文章的更新可以在http://www.ociweb.com/mark/clojure/找到, 同時你也可以在http://www.ociweb.com/mark/stm/找到有關Software Transactional Memory的介紹, 以及Clojure對STM的實現。
這篇文章里面的代碼示例里面通常會以注釋的形式說明每行代碼的結果/輸出,看下面的例子:
幫助| 1 2 | (+ 1 2) ; showing return value: 3 (println "Hello") ; return nil, showing output:Hello |
回到上面
函數式編程
函數式編程是一種強調函數必須被當成第一等公民對待, 并且這些函數是“純”的編程方式。這是受lambda表達式啟發的。純函數的意思是同一個函數對于同樣同樣的參數,它的返回值始終是一樣的 — 而不會因為前一次調用修改了某個全局變量而使得后面的調用和前面調用的結果不一樣。這使得這種程序十分容易理解、調試、測試。它們沒有副作用 — 修改某些全局變量, 進行一些IO操作(文件IO和數據庫)。狀態被維護在方法的參數上面, 而這些參數被存放在棧(stack)上面(通常通過遞歸調用), 而不是被維護在全局的堆(heap)上面。這使得方面可以被執行多次而不用擔心它會更改什么全局的狀態(這是非常重要的特征,等我們討論事務的時候你就會意識到了)。這也使得高級編譯器為了提高代碼性能而對代碼進行重排(reording)和并行化(parallelizing)成為可能。(并行化代碼現在還很少見)
在實際生活中,我們的程序是需要一定的副作用的。Haskel的主力開發Simon Peyton-Jones曾經曰過:
“到最后,任何程序都需要修改狀態,一個沒有副作用的程序對我們來說只是一個黑盒, 你唯一可以感覺到的是:這個黑盒在變熱。。”(http://oscon.blip.tv/file/324976)
問題的關鍵是我們要控制副作用的范圍, 清晰地定位它們,避免這種副作用在代碼里面到處都是。
把函數當作“第一公民”的語言可以把函數賦值給一個變量,作為參數來調用別的函數, 同時一個函數也可以返回一個函數。可以把函數作為返回值的能力使得我們選擇之后程序的行為。接受函數作為參數的函數我們稱為“高階函數”。從某個方面來說,高階函數的行為是由傳進來的函數來配置的,這個函數可以被執行任意次,也可以從不執行。
函數式語言里面的數據是不可修改的。這使得多個線程可以在不用鎖的情況下并發地訪問這個數據。因為數據不會改變,所以根本不需要上鎖。隨著多核處理器的越發流行,函數式語言對并發語言的簡化可能是它最大的優點。如果所有這些聽起來對你來說很有吸引力而且你準備來學學函數式語言,那么你要有點心理準備。許多人覺得函數式語言并不比面向對象的語言難,它們只是風格不同罷了。而花些時間學了函數式語言之后可以得到上面說到的那些好處,我想還是值得的。比較流行的函數式語言有:Clojure, Common Lisp, Erlang, F#, Haskell, ML, OCaml, Scheme, Scala. Clojure和Scala是Java Virtual Machine (JVM)上的語言. 還有一些其它基于JVM的語言: Armed Bear Common Lisp (ABCL), OCaml-Java and Kawa (Scheme).
回到上面
Clojure概述
Clojure是一個動態類型的,運行在JVM(JDK5.0以上),并且可以和java代碼互操作的函數式語言。這個語言的主要目標之一是使得編寫一個有多個線程并發訪問數據的程序變得簡單。
Clojure的發音和單詞closure是一樣的。Clojure之父是這樣解釋Clojure名字來歷的
“我想把這就幾個元素包含在里面: C (C#), L (Lisp) and J (Java). 所以我想到了 Clojure, 而且從這個名字還能想到closure;它的域名又沒有被占用;而且對于搜索引擎來說也是個很不錯的關鍵詞,所以就有了它了.”
很快Clojure就會移植到.NET平臺上了. ClojureCLR是一個運行在Microsoft的CLR的Clojure實現. 在我寫這個入門教程的時候ClojureCLR已經處于alpha階段了.
在2011年7月, ClojureScript項目開始了,這個項目把Clojure代碼編譯成Javascript代碼:看這里https://github.com/clojure/clojurescript.
Clojure是一個開源語言, licence:Eclipse Public License v 1.0 (EPL). This is a very liberal license. 關于EPL的更多信息看這里:http://www.eclipse.org/legal/eplfaq.php .
運行在JVM上面使得Clojure代碼具有可移植性,穩定性,可靠的性能以及安全性。同時也使得我們的Clojure代碼可以訪問豐富的已經存在的java類庫:文件 I/O, 多線程, 數據庫操作, GUI編程, web應用等等等等.
Clojure里面的每個操作被實現成以下三種形式的一種: 函數(function), 宏(macro)或者special form. 幾乎所有的函數和宏都是用Clojure代碼實現的,它們的主要區別我們會在后面解釋。Special forms不是用clojure代碼實現的,而且被clojure的編譯器識別出來. special forms的個數是很少的, 而且現在也不能再實現新的special forms了. 它們包括:catch,def,do,dot (‘.’),finally,fn,if,let,loop,monitor-enter,monitor-exit,new,quote,recur,set!,throw,try 和var.
Clojure提供了很多函數來操作序列(sequence), 而序列是集合的邏輯視圖。很多東西可以被看作序列:Java集合, Clojure的集合, 字符串, 流, 文件系統結構以及XML樹. 從已經存在的clojure集合來創建新的集合的效率是非常高的,因為這里使用了persistent data structures的技術(這對于clojure在數據不可更改的情況下,同時要保持代碼的高效率是非常重要的)。
Clojure提供三種方法來安全地共享可修改的數據。所有三種方法的實現方式都是持有一個可以開遍的引用指向一個不可改變的數據。Refs 通過使用Software Transactional Memory(STM)來提供對于多塊共享數據的同步訪問。Atoms 提供對于單個共享數據的同步訪問。Agents 提供對于單個共享數據的異步訪問。這個我們會在 “引用類型”一節詳細討論。
Clojure是Lisp的一個方言. 但是Clojure對于傳統的Lisp有所發展。比如, 傳統Lisp使用car 來獲取鏈表里面的第一個數據。而Clojure使用first。有關更多Clojure和Lisp的不同看這里: http://clojure.org/lisps.
Lisp的語法很多人很喜歡,很多人很討厭, 主要因為它大量的使用圓括號以及前置表達式. 如果你不喜歡這些,那么你要考慮一下是不是要學習Clojure了 。許多文件編輯器以及IDE會高亮顯示匹配的圓括號, 所以你不用擔心需要去人肉數有沒有多加一個左括號,少寫一個右括號. 同時Clojure的代碼還要比java代碼簡潔. 一個典型的java方法調用是這樣的:
幫助| 1 | methodName(arg1, arg2, arg3); |
而Clojure的方法調用是這樣的:
幫助| 1 | (function-name arg1 arg2 arg3) |
左括號被移到了最前面;逗號和分號不需要了. 我們稱這種語法叫: “form”. 這種風格是簡單而又美麗:Lisp里面所有東西都是這種風格的.要注意的是clojure里面的命名規范是小寫單詞,如果是多個單詞,那么通過中橫線連接。
定義函數也比java里面簡潔。Clojure里面的println 會在它的每個參數之間加一個空格。如果這個不是你想要的,那么你可以把參數傳給str,然后再傳給println .
幫助| 1 2 3 4 | // Java public void hello(String name) { ????System.out.println("Hello, " + name); } |
| 1 2 3 | ; Clojure (defn hello [name] ??(println "Hello," name)) |
Clojure里面大量之用了延遲計算. 這使得只有在我們需要函數結果的時候才去調用它. “懶惰序列” 是一種集合,我們之后在需要的時候才會計算這個集合理解面的元素. 只使得創建無限集合非常高效.
對Clojure代碼的處理分為三個階段:讀入期,編譯期以及運行期。在讀入期,讀入期會讀取clojure源代碼并且把代碼轉變成數據結構,基本上來說就是一個包含列表的列表的列表。。。。在編譯期,這些數據結構被轉化成java的bytecode。在運行期這些java bytecode被執行。函數只有在運行期才會執行。而宏在編譯期就被展開成實際對應的代碼了。
Clojure代碼很難理解么?想想每次你看到java代碼里面那些復雜語法比如: if,for , 以及匿名內部類, 你需要停一下來想想它們到底是什么意思(不是那么的直觀),同時如果想要做一個高效的Java工程師,我們有一些工具可以利用來使得我們的代碼更容易理解。同樣的道理,Clojure也有類似的工具使得我們可以更高效的讀懂clojure代碼。比如:let,apply,map,filter,reduce 以及匿名函數 … 所有這些我們會在后面介紹.
回到上面
讓我們開始吧
Clojure是一個相對來說很新的語言。在經過一些年的努力之后,Clojure的第一版是在2007年10月16日發布的。Clojure的主要部分被稱為 “Clojure proper” 或者 “core”。你可以從這里下載:http://clojure.org/downloads. 你也可以使用Leiningen。最新的源代碼可以從它的Git庫下載.
“Clojure Contrib“是一個大家共享的類庫列表。其中有些類庫是成熟的,被廣泛使用的并且最終可能會被加入Clojure Proper的。但是也有些庫不是很成熟,沒有被廣泛使用,所以也就不會被包含在Conjure Proper里面。所以Clojure Proper里面是魚龍混雜,使用的時候要自己斟酌,文檔在這里:http://richhickey.github.com/clojure-contrib/index.html
對于一個Clojure Contrib, 有三種方法可以得到對應的jar包. 首先你可以下載一個打包好的jar包。其次你可以用maven 來自己打個jar包. Maven可以從這里下載http://maven.apache.org/. 打包命令是 “mvn package“. 再其次你可以用ant. ant可以從這里下載http://ant.apache.org/。命令是: “ant -Dclojure.jar={path}“.
要從最小的源代碼來編譯clojure, 我們假設你已經安裝了Git 和Ant , 運行下面的命令來下載并且編譯打包Clojure Proper和Clojure Contrib:
幫助| 1 2 3 4 5 6 7 | git clone git://github.com/richhickey/clojure.git cd clojure ant clean jar cd .. git clone git://github.com/richhickey/clojure-contrib.git cd clojure-contrib ant -Dclojure.jar=../clojure/clojure.jar clean jar |
下一步,寫一個腳本來運行Read/Eval/Print Loop (REPL) 以及運行 Clojure 程序. 這個腳本通常被命名為”clj”. 怎么使用REPL我們等會再介紹. Windows下面,最簡單的clj腳本是這樣的(UNIX, Linux以及 Mac OS X下面把 %1 改成 $1):
幫助| 1 | java -jar /path/clojure.jar %1 |
這個腳本假定java 在你的PATH 環境變量里面. 為了讓這個腳本更加有用:
- 把經常使用的JAR包比如 “Clojure Contrib” 以及數據庫driver添加到classpath里面去(-cp).
- 使clj更好用:用rlwrap(利用keystrokes來支持的) 或者JLine來得到命令提示以及命令歷史提示。
- 添加一個啟動腳本來設置一些特殊變量(比如*print-length*和 *print-level*), 加載一些常用的、不再java.lang 里面的包 加載一些常用的不再clojure.core里面的函數并且定義一些常用自定義的函數.
使用這個腳本來啟動REPL我們會等會介紹. 用下面這個命令來運行一個clojure腳本(通常以clj為后綴名):
幫助| 1 | clj source-file-path |
更多細節看這里http://clojure.org/getting_started 以及這里:http://clojure.org/repl_and_main。同時Stephen Gilardi 還提供了一個腳本:http://github.com/richhickey/clojure-contrib/raw/master/launchers/bash/clj-env-dir。
為了更充分的利用機器的多核,你應該這樣來調用: “java -server ...“.
提供給Clojure的命令行參數被封裝在預定義的變量*command-line-args*里面。
回到上面
Clojure語法
Lisp方言有一個非常簡潔的語法 — 有些人覺得很美的語法。數據和代碼的表達形式是一樣的,一個列表的列表很自然地在內存里面表達成一個tree。(a b c)表示一個對函數a的調用,而參數是b和c。如果要表示數據,你需要使用'(a b c) o或者(quote (a b c))。通常情況下就是這樣了,除了一些特殊情況 — 到底有多少特殊情況取決于你所使用的方言。
我們把這些特殊情況稱為語法糖。語法糖越多代碼寫起來越簡介,但是同時我們也要學習更多的東西以讀懂這些代碼。這需要找到一個平衡點。很多語法糖都有對應的函數可以調用。到底語法糖是多了還是少了還是你們自己來判斷吧。
下面這個表格簡要地列舉了Clojure里面的一些語法糖, 這些語法糖我們會在后面詳細講解的,所以如果你現在沒辦法完全理解它們不用擔心。
| 注釋 | ; text? 單行注釋 | 宏(comment text)可以用來寫多行注釋 |
| 字符 (Java char 類型) | \char \tab \newline \space \uunicode-hex-value | (char ascii-code) (char \uunicode) |
| 字符串 (Java String 對象) | "text" | (str char1 char2 ...) 可以把各種東西串成一個字符串 |
| 關鍵字是一個內部字符串; 兩個同樣的關鍵字指向同一個對象; 通常被用來作為map的key | :name | (keyword "name") |
| 當前命名空間的關鍵字 | ::name | N/A |
| 正則表達式 | #"pattern" | (re-pattern pattern) |
| 逗號被當成空白(通常用在集合里面用來提高代碼可讀性) | , (逗號) | N/A |
| 鏈表(linked list) | '(items) (不會evaluate每個元素) | (list items) 會evaluate每個元素 |
| vector(和數組類似) | [items] | (vector items) |
| set | #{items} 建立一個hash-set | (hash-set items) (sorted-set items) |
| map | {key-value-pairs} 建立一個hash-map | (hash-map key-value-pairs) (sorted-map key-value-pairs) |
| 給symbol或者集合綁定元數據 | #^{key-value-pairs} object 在讀入期處理 | (with-meta object metadata-map) 在運行期處理 |
| 獲取symbol或者集合的元數據 | ^object | (meta object) |
| 獲取一個函數的參數列表(個數不定的) | & name | N/A |
| 函數的不需要的參數的默認名字 | _ (下劃線) | N/A |
| 創建一個java對象(注意class-name后面的點) | (class-name. args) | (new class-name args) |
| 調用java方法 | (. class-or-instance method-name args) 或者 (.method-name class-or-instance args) | N/A |
| 串起來調用多個函數,前面一個函數的返回值會作為后面一個函數的第一個參數;你還可以在括號里面指定額外參數;注意前面的兩個點 | (.. class-or-object (method1 args) (method2 args) ...) | N/A |
| 創建一個匿名函數 | #(single-expression) 用% (等同于 %1), %1, %2來表示參數 | (fn [arg-names] expressions) |
| 獲取Ref, Atom 和Agent對應的valuea | @ref | (deref ref) |
| get Var object instead of the value of a symbol (var-quote) | #'name | (var name) |
| syntax quote (使用在宏里面) | ` | none |
| unquote (使用在宏里面) | ~value | (unquote value) |
| unquote splicing (使用在宏里面) | ~@value | none |
| auto-gensym (在宏里面用來產生唯一的symbol名字) | prefix# | (gensym prefix ) |
對于二元操作符比如 +和*, Lisp方言使用前置表達式而不是中止表達式,這和一般的語言是不一樣的。比如在java里面你可能會寫a + b + c, 而在Lisp里面它相當于(+ a b c) 。這種表達方式的一個好處是如果操作數有多個,那么操作符只用寫一次. 其它語言里面的二元操作符在lisp里面是函數,所以可以有多個操作數。
Lisp代碼比其它語言的代碼有更多的小括號的一個原因是Lisp里面不使用其它語言使用的大括號,比如在java里面,方法代碼是被包含在大括號里面的,而在lisp代碼里面是包含在小括號里面的。
比較下面兩段簡單的Java和Clojure代碼,它們實現相同的功能。它們的輸出都是: “edray” 和 “orangeay”.
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | // This is Java code. public class PigLatin { ?? ????public static String pigLatin(String word) { ????????char firstLetter = word.charAt(0); ????????if ("aeiou".indexOf(firstLetter) != -1) return word + "ay"; ????????return word.substring(1) + firstLetter + "ay"; ????} ?? ????public static void main(String args[]) { ????????System.out.println(pigLatin("red")); ????????System.out.println(pigLatin("orange")); ????} } |
?
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | ; This is Clojure code. ; When a set is used as a function, it returns a boolean ; that indicates whether the argument is in the set. (def vowel? (set "aeiou")) ?? (defn pig-latin [word] ; defines a function ??; word is expected to be a string ??; which can be treated like a sequence of characters. ??(let [first-letter (first word)] ; assigns a local binding ????(if (vowel? first-letter) ??????(str word "ay") ; then part of if ??????(str (subs word 1) first-letter "ay")))) ; else part of if ?? (println (pig-latin "red")) (println (pig-latin "orange")) |
Clojure支持所有的常見數據類型比如 booleans (true and false), 數字, 高精度浮點數, 字符(上面表格里面提到過 ) 以及字符串. 同時還支持分數 — 不是浮點數,因此在計算的過程中不會損失精度.
Symbols是用來給東西命名的. 這些名字是被限制在名字空間里面的,要么是指定的名字空間,要么是當前的名字空間. Symbols的值是它所代表的名字的值. 要使用Symbol的值,你必須把它用引號引起來.
關鍵字以冒號打頭,被用來當作唯一標示符,通常用在map里面 (比如:red, :green和 :blue).
和任何語言一樣,你可以寫出很難懂的Clojure代碼。遵循一些最佳實踐可以避免這個。寫一些簡短的,專注自己功能的函數可以使函數變得容易讀,測試以及重復利用。經常使用“抽取方法”的模式來對你的代碼進行重構。高度內嵌的函數是非常難懂得,千萬不要這么寫, 你可以使用let來幫助你。把匿名函數傳遞給命名函數是非常常見的,但是不要把一個匿名函數傳遞給另外一個匿名函數, 這樣代碼就很難懂了。
?
REPL
REPL 是read-eval-print loop的縮寫. 這是Lisp的方言提供給用戶的一個標準交互方式,如果用過python的人應該用過這個,你輸入一個表達式,它立馬再給你輸出結果,你再輸入。。。如此循環。這是一個非常有用的學習語言,測試一些特性的工具。
為了啟動REPL, 運行我們上面寫好的clj腳本。成功的話會顯示一個”user=>“. “=>” 前面的字符串表示當前的默認名字空間。“=>”后面的則是你輸入的form以及它的輸出結果。 下面是個簡單的例子:
幫助| 1 2 3 4 | user=> (def n 2) #'user/n user=> (* n 3) 6 |
def 是一個 special form, 它相當于java里面的定義加賦值語句. 它的輸出表示一個名字叫 “n” 的symbol被定義在當前的名字空間 “user” 里面。
要查看一個函數,宏或者名字空間的文檔輸入(doc name)。看下面的例子:
幫助| 1 2 3 4 5 6 7 | (require 'clojure.contrib.str-utils) (doc clojure.contrib.str-utils/str-join) ; -> ; ------------------------- ; clojure.contrib.str-utils/str-join ; ([separator sequence]) ;?? Returns a string of all elements in 'sequence', separated by ;?? 'separator'.? Like Perl's 'join'. |
如果要找所有包含某個字符串的所有的函數的,宏的文檔,那么輸入這個命令(find-doc "text").
如果要查看一個函數,宏的源代碼(source name).source 是一個定義在clojure.contrib.repl-utils 名字空間里面的宏,REPL會自動加載這個宏的。
如果要加載并且執行文件里面的clojure代碼那么使用這個命令(load-file "file-path"). Clojure源文件一般以.clj作為后綴。
如果要退出REPL,在Windows下面輸出ctrl-z然后回車, 或者直接 ctrl-c; 在其它平臺下 (包括UNIX, Linux 和 Mac OS X), 輸入 ctrl-d.
回到上面
Bindings
Clojure里面是不支持變量的。它跟變量有點像,但是在被賦值之前是不允許改的,包括:全局binding, 線程本地(thread local)binding, 以及函數內的本地binding, 以及一個表達式內部的binding。
def 這個special form 定義一個全局的 binding,并且你還可以給它一個”root value” ,這個root value在所有的線程里面都是可見的,除非你給它賦了一個線程本地的值.def 也可以用來改變一個已經存在的binding的root value —— 但是這是不被鼓勵的,因為這會犧牲不可變數據所帶來的好處。
函數的參數是只在這個函數內可見的本地binding。
let 這個special form 創建局限于一個 當前form的bindings. 它的第一個參數是一個vector, 里面包含名字-表達式的對子。表達式的值會被解析然后賦給左邊的名字。這些binding可以在這個vector后面的表達式里面使用。這些binding還可以被多次賦值以改變它們的值,let命令剩下的參數是一些利用這個binding來進行計算的一些表達式。注意:如果這些表達式里面有調用別的函數,那么這個函數是無法利用let創建的這個binding的。
宏 binding 跟let 類似, 但是它創建的本地binding會暫時地覆蓋已經存在的全局binding. 這個binding可以在創建這個binding的form以及這個form里面調用的函數里面都能看到。但是一旦跳出了這個binding 那么被覆蓋的全局binding的值會回復到之前的狀態。
從 Clojure 1.3開始, binding只能用在 動態變量(dynamic var)上面了. 下面的例子演示了怎么定一個dynamic var。另一個區別是let 是串行的賦值的, 所以后面的binding可以用前面binding的值, 而binding 是不行的.
要被用來定義成新的、本地線程的、用binding來定義的binding有它們自己的命名方式:她們以星號開始,以星號結束。在這篇文章里面你會看到:*command-line-args*,*agent*,*err*,*flush-on-newline*,*in*,*load-tests*,*ns*,*out*,*print-length*,*print-level* and*stack-trace-depth*.要使用這些binding的函數會被這些binding的值影響的。比如給*out*一個新的binding會改變println函數的輸出終端。
下面的例子介紹了def,let 和binding的用法。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | (def ^:dynamic v 1) ; v is a global binding ?? (defn f1 [] ??(println "f1: v =" v)) ; global binding ?? (defn f2 [] ??(println "f2: before let v =" v) ; global binding ??(let [v 2] ; creates local binding v that shadows global one ????(println "f2: in let, v =" v) ; local binding ????(f1)) ??(println "f2: after let v =" v)) ; global binding ?? (defn f3 [] ??(println "f3: before binding v =" v) ; global binding ??(binding [v 3] ; same global binding with new, temporary value ????(println "f3: in binding, v =" v) ; global binding ????(f1)) ??(println "f3: after binding v =" v)) ; global binding ?? (defn f4 [] ?(def v 4)) ; changes the value of the global binding ?? (f2) (f3) (f4) (println "after calling f4, v =" v) |
上面代碼的輸出是這樣的:
幫助| 1 2 3 4 5 6 7 8 9 | f2: before let v = 1 f2: in let, v = 2 f1: v = 1 (let DID NOT change value of global binding) f2: after let v = 1 f3: before binding v = 1 f3: in binding, v = 3 f1: v = 3 (binding DID change value of global binding) f3: after binding v = 1 (value of global binding reverted back) after calling f4, v = 4 |
回到上面
集合
Clojure提供這些集合類型: list, vector, set, map。同時Clojure還可以使用Java里面提供的將所有的集合類型,但是通常不會這樣做的, 因為Clojure自帶的集合類型更適合函數式編程。
Clojure集合有著java集合所不具備的一些特性。所有的clojure集合是不可修改的、異源的以及持久的。不可修改的意味著一旦一個集合產生之后,你不能從集合里面刪除一個元素,也往集合里面添加一個元素。異源的意味著一個集合里面可以裝進任何東西(而不必須要這些東西的類型一樣)。持久的以為著當一個集合新的版本產生之后,舊的版本還是在的。CLojure以一種非常高效的,共享內存的方式來實現這個的。比如有一個map里面有一千個name-valuea pair, 現在要往map里面加一個,那么對于那些沒有變化的元素, 新的map會共享舊的map的內存,而只需要添加一個新的元素所占用的內存。
有很多核心的函數可以用來操作所有這些類型的集合。。多得以至于無法在這里全部描述。其中的一小部分我們會在下面介紹vector的時候介紹一下。要記住的是,因為clojure里面的集合是不可修改的,所以也就沒有對集合進行修改的函數。相反clojure里面提供了一些函數來從一個已有的集合來高效地創建新的集合 — 使用persistent data structures。同時也有一些函數操作一個已有的集合(比如vector)來產生另外一種類型的集合(比如LazySeq), 這些函數有不同的特性。
提醒: 這一節里面介紹的Clojure集合對于學習clojure來說是非常的重要。但是這里介紹一個函數接著一個函數,所以你如果覺得有點煩,有點乏味,你可以跳過,等用到的時候再回過頭來查詢。
count 返回集合里面的元素個數,比如:
幫助| 1 | (count [19 "yellow" true]) ; -> 3 |
conj 函數是 conjoin的縮寫, 添加一個元素到集合里面去,到底添加到什么位置那就取決于具體的集合了,我們會在下面介紹具體集合的時候再講。
reverse 把集合里面的元素反轉。
幫助| 1 | (reverse [2 4 7]) ; -> (7 4 2) |
map 對一個給定的集合里面的每一個元素調用一個指定的方法,然后這些方法的所有返回值構成一個新的集合(LazySeq)返回。這個指定了函數也可以有多個參數,那么你就需要給map多個集合了。如果這些給的集合的個數不一樣,那么執行這個函數的次數取決于個數最少的集合的長度。比如:
幫助| 1 2 3 | ; The next line uses an anonymous function that adds 3 to its argument. (map #(+ % 3) [2 4 7]) ; -> (5 7 10) (map + [2 4 7] [5 6] [1 2 3 4]) ; adds corresponding items -> (8 12) |
apply 把給定的集合里面的所有元素一次性地給指定的函數作為參數調用,然后返回這個函數的返回值。所以apply與map的區別就是map返回的還是一個集合,而apply返回的是一個元素, 可以把apply看作是SQL里面的聚合函數。比如:
幫助| 1 | (apply + [2 4 7]); -> 13 |
有很多函數從一個集合里面獲取一個元素,比如:
幫助| 1 2 3 4 5 | (def stooges ["Moe" "Larry" "Curly" "Shemp"]) (first stooges) ; -> "Moe" (second stooges) ; -> "Larry" (last stooges) ; -> "Shemp" (nth stooges 2) ; indexes start at 0 -> "Curly" |
也有一些函數從一個集合里面獲取多個元素,比如:
幫助| 1 2 3 4 5 6 | (next stooges) ; -> ("Larry" "Curly" "Shemp") (butlast stooges) ; -> ("Moe" "Larry" "Curly") (drop-last 2 stooges) ; -> ("Moe" "Larry") ; Get names containing more than three characters. (filter #(> (count %) 3) stooges) ; -> ("Larry" "Curly" "Shemp") (nthnext stooges 2) ; -> ("Curly" "Shemp") |
有一些謂詞函數測試集合里面每一個元素然后返回一個布爾值,這些函數都是”short-circuit”的,一旦它們的返回值能確定它們就不再繼續測試剩下的元素了,有點像java的&&和or, 比如:
幫助| 1 2 3 4 | (every? #(instance? String %) stooges) ; -> true (not-every? #(instance? String %) stooges) ; -> false (some #(instance? Number %) stooges) ; -> nil (not-any? #(instance? Number %) stooges) ; -> true |
Lists
Lists是一個有序的元素的集合 — 相當于java里面的LinkedList。這種集合對于那種一直要往最前面加一個元素,干掉最前面一個元素是非常高效的(O(1)) — 想到于java里面的堆棧, 但是沒有高效的方法來獲取第N個元素, 也沒有高效的辦法來修改第N個元素。
下面是創建同樣的list的多種不同的方法:
幫助| 1 2 3 | (def stooges (list "Moe" "Larry" "Curly")) (def stooges (quote ("Moe" "Larry" "Curly"))) (def stooges '("Moe" "Larry" "Curly")) |
some 可以用來檢測一個集合是否含有某個元素. 它的參數包括一個謂詞函數以及一個集合。你可以能會想了,為了要看一個list到底有沒有某個元素為什么要指定一個謂詞函數呢?其實我們是故意這么做來讓你盡量不要這么用的。從一個list里面搜索一個元素是線性的操作(不高效),而要從一個set里面搜索一個元素就容易也高效多了,看下面的例子對比:
幫助| 1 2 3 4 5 | (some #(= % "Moe") stooges) ; -> true (some #(= % "Mark") stooges) ; -> nil ; Another approach is to create a set from the list ; and then use the contains? function on the set as follows. (contains? (set stooges) "Moe") ; -> true |
conj 和cons 函數的作用都是通過一個已有的集合來創建一個新的包含更多元素的集合 — 新加的元素在最前面。remove 函數創建一個只包含所指定的謂詞函數測試結果為false的元素的集合:
幫助| 1 2 | (def more-stooges (conj stooges "Shemp")) ->; ("Shemp" "Moe" "Larry" "Curly") (def less-stooges (remove #(= % "Curly") more-stooges)) ; -> ("Shemp" "Moe" "Larry") |
into 函數把兩個list里面的元素合并成一個新的大list
幫助| 1 2 3 4 | (def kids-of-mike '("Greg" "Peter" "Bobby")) (def kids-of-carol '("Marcia" "Jan" "Cindy")) (def brady-bunch (into kids-of-mike kids-of-carol)) (println brady-bunch) ; -> (Cindy Jan Marcia Greg Peter Bobby) |
peek 和pop 可以用來把list當作一個堆棧來操作. 她們操作的都是list的第一個元素。
Vectors
Vectors也是一種有序的集合。這種集合對于從最后面刪除一個元素,或者獲取最后面一個元素是非常高效的(O(1))。這意味著對于向vector里面添加元素使用conj被使用cons更高效。Vector對于以索引的方式訪問某個元素(用nth命令)或者修改某個元素(用assoc)來說非常的高效。函數定義的時候指定參數列表用的就是vector。
下面是兩種創建vector的方法:
幫助| 1 2 | (def stooges (vector "Moe" "Larry" "Curly")) (def stooges ["Moe" "Larry" "Curly"]) |
除非你要寫的程序要特別用到list的從前面添加/刪除效率很高的這個特性, 否則一般來說我們鼓勵你們用vector而不是lists。這主要是因為語法上[...] 比 ‘(...) 更自然,更不容易弄混淆。因為函數,宏以及special form的語法也是(...)。
get 獲取vector里面指定索引的元素. 我們后面會看到get也可以從map里面獲取指定key的value。索引是從0開始的。get 函數和函數nth 類似. 它們都接收一個可選的默認值參數 — 如果給定的索引超出邊界,那么會返回這個默認值。如果沒有指定默認值而索引又超出邊界了,get 函數會返回nil 而nth 會拋出一個異常. 看例子:
幫助| 1 2 | (get stooges 1 "unknown") ; -> "Larry" (get stooges 3 "unknown") ; -> "unknown" |
assoc 可以對 vectors 和 maps進行操作。 當用在 vector上的時候, 它會從給定的vector創建一個新的vector, 而指定的那個索引所對應的元素被替換掉。如果指定的這個索引等于vector里面元素的數目,那么我們會把這個元素加到新vector的最后面去;如果指定的索引比vector的大小要大,那么一個IndexOutOfBoundsException 異常會被拋出來。看代碼:
幫助| 1 | (assoc stooges 2 "Shemp") ; -> ["Moe" "Larry" "Shemp"] |
subvec 獲取一個給定vector的子vector。它接受三個參數,一個vectore, 一個起始索引以及一個可選的結束索引。如果結束索引沒有指定,那么默認的結束索引就是vector的大小。新的vector和原來的vector共享內存(所以高效)。
所有上面的對于list的例子代碼對于vector同樣適用。peek 和pop 函數對于vector同樣適用, 只是它們操作的是vector的最后一個元素,而對于list操作的則是第一個函數。conj 函數從一個給定的vector創建一個新的vector — 添加一個元素到新的vector的最后面去. cons 函數從一個給定的vector創建一個新的vector — 添加一個新的元素到vector的最前面去。
Sets
Sets 是一個包含不重復元素的集合。當我們要求集合里面的元素不可以重復,并且我們不要求集合里面的元素保持它們添加時候的順序,那么sets是比較適合的。 Clojure 支持兩種不同的set: 排序的和不排序的。如果添加到set里面的元素相互之間不能比較大小,那么一個ClassCastException 異常會被拋出來。下面是一些創建set的方法:
幫助| 1 2 3 | (def stooges (hash-set "Moe" "Larry" "Curly")) ; not sorted (def stooges #{"Moe" "Larry" "Curly"}) ; same as previous (def stooges (sorted-set "Moe" "Larry" "Curly")) |
contains? 函數可以操作sets和maps. 當操作set的時候, 它返回給定的set是否包含某個元素。這比在list和vector上面使用的some函數就簡單多了. 看例子:
幫助| 1 2 | (contains? stooges "Moe") ; -> true (contains? stooges "Mark") ; -> false |
Sets 可以被當作它里面的元素的函數來使用. 當以這種方式來用的時候,返回值要么是這個元素,要么是nil. 這個比起contains?函數來說更簡潔. 比如:
幫助| 1 2 3 | (stooges "Moe") ; -> "Moe" (stooges "Mark") ; -> nil (println (if (stooges person) "stooge" "regular person")) |
在介紹list的時候提到的函數conj 和into 對于set也同樣適用. 只是元素的順序只有對sorted-set才有定義.
disj 函數通過去掉給定的set里面的一些元素來創建一個新的set. 看例子:
幫助| 1 2 | (def more-stooges (conj stooges "Shemp")) ; -> #{"Moe" "Larry" "Curly" "Shemp"} (def less-stooges (disj more-stooges "Curly")) ; -> #{"Moe" "Larry" "Shemp"} |
你也可以看看clojure.set 名字空間里面的一些函數:difference,index,intersection,join,map-invert,project,rename,rename-keys,select 和union. 其中有些函數的操作的對象是map而不是set。
Maps
Maps 保存從key到value的a對應關系 — key和value都可以是任意對象。key-value 組合被以一種可以按照key的順序高效獲取的方式保存著。
下面是創建map的一些方法, 其中逗號是為了提高可讀性的,它是可選的,解析的時候會被當作空格忽略掉的。
幫助| 1 2 3 4 5 6 | (def popsicle-map ??(hash-map :red :cherry, :green :apple, :purple :grape)) (def popsicle-map ??{:red :cherry, :green :apple, :purple :grape}) ; same as previous (def popsicle-map ??(sorted-map :red :cherry, :green :apple, :purple :grape)) |
Map可以作為它的key的函數,同時如果key是keyword的話,那么key也可以作為map的函數。下面是三種獲取:green所對應的值的方法:
幫助| 1 2 3 | (get popsicle-map :green) (popsicle-map :green) (:green popsicle-map) |
contains? 方法可以操作 sets 和 maps. 當被用在map上的時候,它返回map是否包含給定的key. keys 函數返回map里面的所有的key的集合. vals 函數返回map里面所有值的集合. 看例子:
幫助| 1 2 3 | (contains? popsicle-map :green) ; -> true (keys popsicle-map) ; -> (:red :green :purple) (vals popsicle-map) ; -> (:cherry :apple :grape) |
assoc 函數可以操作 maps 和 vectors. 當被用在map上的時候,它會創建一個新的map, 同時添加任意對新的name-value pair, 如果某個給定的key已經存在了,那么它的值會被更新。看例子:
幫助| 1 2 | (assoc popsicle-map :green :lime :blue :blueberry) ; -> {:blue :blueberry, :green :lime, :purple :grape, :red :cherry} |
dissoc 創建一個新的map, 同時忽略掉給定的那么些key, 看例子:
幫助| 1 | (dissoc popsicle-map :green :blue) ; -> {:purple :grape, :red :cherry} |
我們也可以把map看成一個簡單的集合,集合里面的每個元素是一個pair: name-value:clojure.lang.MapEntry 對象. 這樣就可以和doseq 跟destructuring一起使用了, 它們的作用都是更簡單地來遍歷map, 我們會在后面詳細地介紹這些函數. 下面的這個例子會遍歷popsicle-map 里面的所有元素,把key bind到color, 把value bind到flavor。 name函數返回一個keyword的字符串名字。
幫助| 1 2 3 | (doseq [[color flavor] popsicle-map] ??(println (str "The flavor of " (name color) ????" popsicles is " (name flavor) "."))) |
上面的代碼的輸出是這樣的:
幫助| 1 2 3 | The flavor of green popsicles is apple. The flavor of purple popsicles is grape. The flavor of red popsicles is cherry. |
select-keys 函數接收一個map對象,以及一個key的集合的參數,它返回這個集合里面key在那個集合里面的一個子map。看例子:
幫助| 1 | (select-keys popsicle-map [:red :green :blue]) ; -> {:green :apple, :red :cherry} |
conj 函數添加一個map里面的所有元素到另外一個map里面去。如果目標map里面的key在源map里面也有,那么目標map的值會被更新成源map里面的值。
map里面的值也可以是一個map, 而且這樣嵌套無限層。獲取嵌套的值是非常簡單的。同樣的,更新一個嵌套的值也是很簡單的。
為了證明這個, 我們會創建一個描述人(person)的map。其中內嵌了一個表示人的地址的map,同時還有一個叫做employer的內嵌map。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | (def person { ??:name "Mark Volkmann" ??:address { ????:street "644 Glen Summit" ????:city "St. Charles" ????:state "Missouri" ????:zip 63304} ??:employer { ????:name "Object Computing, Inc." ????:address { ??????:street "12140 Woodcrest Executive Drive, Suite 250" ??????:city "Creve Coeur" ??????:state "Missouri" ??????:zip 63141}}}) |
get-in 函數、宏-> 以及函數reduce 都可以用來獲得內嵌的key. 下面展示了三種獲取這個人的employer的address的city的值的方法:
幫助| 1 2 3 | (get-in person [:employer :address :city]) (->; person :employer :address :city) ; explained below (reduce get person [:employer :address :city]) ; explained below |
宏-> 我們也稱為 “thread” 宏, 它本質上是調用一系列的函數,前一個函數的返回值作為后一個函數的參數. 比如下面兩行代碼的作用是一樣的:
幫助| 1 2 | (f1 (f2 (f3 x))) (->; x f3 f2 f1) |
在名字空間clojure.contrib.core 里面還有個 -?>宏, 它會馬上返回nil, 如果它的調用鏈上的任何一個函數返回nil (short-circiut)。這會避免拋出NullPointerException異常。
reduce 函數接收一個需要兩個參數的函數, 一個可選的value以及一個集合。它會以value以及集合的第一個元素作為參數來調用給定的函數(如果指定了value的話), 要么以集合的第一個元素以及第二個元素為參數來調用給定的函數(如果沒有指定value的話)。接著就以這個返回值以及集合里面的下一個元素為參數來調用給定的函數,知道集合里面的元素都被計算了 — 最后返回一個值. 這個函數與ruby里面的inject 以及Haskell里面的foldl 作用是一樣的。
assoc-in 函數可以用來修改一個內嵌的key的值。看下面的例子把person的employer->address->city修改成Clayton了。
幫助| 1 | (assoc-in person [:employer :address :city] "Clayton") |
update-in 函數也是用來更新給定的內嵌的key對應的值,只是這個新值是通過一個給定的函數來計算出來。下面的例子里面會把person的employer->address->zip改成舊的zip + “-1234″。看例子:
幫助| 1 | (update-in person [:employer :address :zip] str "-1234") ; using the str function |
回到上面
StructMaps
StructMap和普通的map類似,它的作用其實是用來模擬java里面的javabean, 所以它比普通的map的優點就是,它把一些常用的字段抽象到一個map里面去,這樣你就不用一遍一遍的重復了。并且和java類似,他會幫你生成合適的equals 和hashCode 方法。并且它還提供方式讓你可以創建比普通map里面的hash查找要快的字段訪問方法(javabean里面的getXXX方法)。
create-struct 函數 和defstruct 宏都可以用來定義StructMap, defstruct內部調用的也是create-struct。map的key通常都是用keyword來指定的。看例子:
幫助| 1 2 | (def vehicle-struct (create-struct :make :model :year :color)) ; long way (defstruct vehicle-struct :make :model :year :color) ; short way |
struct 實例化StructMap的一個對象,相當于java里面的new關鍵字. 你提供給struct的參數的順序必須和你定義的時候提供的keyword的順序一致,后面的參數可以忽略, 如果忽略,那么對應key的值就是nil。看例子:
幫助| 1 | (def vehicle (struct vehicle-struct "Toyota" "Prius" 2009)) |
accessor 函數可以創建一個類似java里面的getXXX的方法, 它的好處是可以避免hash查找, 它比普通的hash查找要快。看例子:
幫助| 1 2 3 4 5 6 | ; Note the use of def instead of defn because accessor returns ; a function that is then bound to "make". (def make (accessor vehicle-struct :make)) (make vehicle) ; -> "Toyota" (vehicle :make) ; same but slower (:make vehicle) ; same but slower |
在創建一個StructMap之后, 你還可以給它添加在定義struct的時候沒有指定的key。但是你不能刪除定義時候已經指定的key。
回到上面
定義函數
defn 宏用來定義一個函數。它的參數包括一個函數名字,一個可選的注釋字符串,參數列表,然后一個方法體。而函數的返回值則是方法體里面最后一個表達式的值。所有的函數都會返回一個值, 只是有的返回的值是nil。看例子:
幫助| 1 2 3 4 5 6 | (defn parting ??"returns a String parting" ??[name] ??(str "Goodbye, " name)) ; concatenation ?? (println (parting "Mark")) ; -> Goodbye, Mark |
函數必須先定義再使用。有時候可能做不到, 比如兩個方法項目調用,clojure采用了和C語言里面類似的做法: declare, 看例子:
幫助| 1 | (declare <;em>function-names</em>) |
通過宏defn- 定義的函數是私有的. 這意味著它們只在定義它們的名字空間里面可見. 其它一些類似定義私有函數/宏的還有:defmacro- 和defstruct- (在clojure.contrib.def里面)。
函數的參數個數可以是不定的。可選的那些參數必須放在最后面(這一點跟其它語言是一樣的), 你可以通過加個&符號把它們收集到一個list里面去Functions can take a variable number of parameters. Optional parameters must appear at the end. They are gathered into a list by adding an ampersand and a name for the list at the end of the parameter list.
幫助| 1 2 3 4 | (defn power [base & exponents] ??; Using java.lang.Math static method pow. ??(reduce #(Math/pow %1 %2) base exponents)) (power 2 3 4) ; 2 to the 3rd = 8; 8 to the 4th = 4096 |
函數定義可以包含多個參數列表以及對應的方法體。每個參數列表必須包含不同個數的參數。這通常用來給一些參數指定默認值。看例子:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | (defn parting ??"returns a String parting in a given language" ??([] (parting "World")) ??([name] (parting name "en")) ??([name language] ????; condp is similar to a case statement in other languages. ????; It is described in more detail later. ????; It is used here to take different actions based on whether the ????; parameter "language" is set to "en", "es" or something else. ????(condp = language ??????"en" (str "Goodbye, " name) ??????"es" (str "Adios, " name) ??????(throw (IllegalArgumentException. ????????(str "unsupported language " language)))))) ?? (println (parting)) ; -> Goodbye, World (println (parting "Mark")) ; -> Goodbye, Mark (println (parting "Mark" "es")) ; -> Adios, Mark (println (parting "Mark", "xy")) ; -> java.lang.IllegalArgumentException: unsupported language xy |
匿名函數是沒有名字的。他們通常被當作參數傳遞給其他有名函數(相對于匿名函數)。匿名函數對于那些只在一個地方使用的函數比較有用。下面是定義匿名函數的兩種方法:
幫助| 1 2 3 | (def years [1940 1944 1961 1985 1987]) (filter (fn [year] (even? year)) years) ; long way w/ named arguments -> (1940 1944) (filter #(even? %) years) ; short way where % refers to the argument |
通過fn 定義的匿名函數可以包含任意個數的表達式; 而通過#(...), 定義的匿名函數則只能包含一個表達式,如果你想包含多個表達式,那么把它用do包起來。如果只有一個參數, 那么你可以通過%來引用它; 如果有多個參數, 那么可以通過%1,%2 等等來引用。 看例子:
幫助| 1 2 3 4 5 6 | (defn pair-test [test-fn n1 n2] ??(if (test-fn n1 n2) "pass" "fail")) ?? ; Use a test-fn that determines whether ; the sum of its two arguments is an even number. (println (pair-test #(even? (+ %1 %2)) 3 5)) ; -> pass |
Java里面的方法可以根據參數的類型來進行重載。而Clojure里面則只能根據參數的個數來進行重載。不過Clojure里面的multimethods技術可以實現任意 類型的重載。
宏defmulti 和defmethod 經常被用在一起來定義 multimethod. 宏defmulti 的參數包括一個方法名以及一個dispatch函數,這個dispatch函數的返回值會被用來選擇到底調用哪個重載的函數。宏defmethod 的參數則包括方法名,dispatch的值, 參數列表以及方法體。一個特殊的dispatch值:default 是用來表示默認情況的 — 即如果其它的dispatch值都不匹配的話,那么就調用這個方法。defmethod 多定義的名字一樣的方法,它們的參數個數必須一樣。傳給multimethod的參數會傳給dipatch函數的。
下面是一個用multimethod來實現基于參數的類型來進行重載的例子:
幫助| 1 2 3 4 5 6 7 | (defmulti what-am-i class) ; class is the dispatch function (defmethod what-am-i Number [arg] (println arg "is a Number")) (defmethod what-am-i String [arg] (println arg "is a String")) (defmethod what-am-i :default [arg] (println arg "is something else")) (what-am-i 19) ; -> 19 is a Number (what-am-i "Hello") ; -> Hello is a String (what-am-i true) ; -> true is something else |
因為dispatch函數可以是任意一個函數,所以你也可以寫你自己的dispatch函數。比如一個自定義的dispatch函數可以會根據一個東西的尺寸大小來返回:small,:medium 以及:large。然后對應每種尺寸有一個方法。
下劃線可以用來作為參數占位符 ?– 如果你不要使用這個參數的話。這個特性在回調函數里面比較有用, 因為回調函數的設計者通常想把盡可能多的信息給你, 而你通常可能只需要其中的一部分。看例子:
幫助| 1 2 3 4 5 6 | (defn callback1 [n1 n2 n3] (+ n1 n2 n3)) ; uses all three arguments (defn callback2 [n1 _ n3] (+ n1 n3)) ; only uses 1st & 3rd arguments (defn caller [callback value] ??(callback (+ value 1) (+ value 2) (+ value 3))) (caller callback1 10) ; 11 + 12 + 13 -> 36 (caller callback2 10) ; 11 + 13 -> 24 |
complement 函數接受一個函數作為參數,如果這個參數返回值是true, 那么它就返回false, 相當于一個取反的操作。看例子:
幫助| 1 2 3 | (defn teenager? [age] (and (>;= age 13) (< age 20))) (def non-teen? (complement teenager?)) (println (non-teen? 47)) ; -> true |
comp把任意多個函數組合成一個,前面一個函數的返回值作為后面一個函數的參數。調用的順序是從右到左(注意不是從左到右)看例子:
幫助| 1 2 3 4 5 6 | (defn times2 [n] (* n 2)) (defn minus3 [n] (- n 3)) ; Note the use of def instead of defn because comp returns ; a function that is then bound to "my-composition". (def my-composition (comp minus3 times2)) (my-composition 4) ; 4*2 - 3 -> 5 |
partial 函數創建一個新的函數 — 通過給舊的函數制定一個初始值, 然后再調用原來的函數。比如* 是一個可以接受多個參數的函數,它的作用就是計算它們的乘積,如果我們想要一個新的函數,使的返回結果始終是乘積的2倍,我們可以這樣做:
幫助| 1 2 3 4 | ; Note the use of def instead of defn because partial returns ; a function that is then bound to "times2". (def times2 (partial * 2)) (times2 3 4) ; 2 * 3 * 4 -> 24 |
下面是一個使用 map 和partial 的有趣的例子.
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | (defn- polynomial ??"computes the value of a polynomial ???with the given coefficients for a given value x" ??[coefs x] ??; For example, if coefs contains 3 values then exponents is (2 1 0). ??(let [exponents (reverse (range (count coefs)))] ????; Multiply each coefficient by x raised to the corresponding exponent ????; and sum those results. ????; coefs go into %1 and exponents go into %2. ????(apply + (map #(* %1 (Math/pow x %2)) coefs exponents)))) ?? (defn- derivative ??"computes the value of the derivative of a polynomial ???with the given coefficients for a given value x" ??[coefs x] ??; The coefficients of the derivative function are obtained by ??; multiplying all but the last coefficient by its corresponding exponent. ??; The extra exponent will be ignored. ??(let [exponents (reverse (range (count coefs))) ????????derivative-coefs (map #(* %1 %2) (butlast coefs) exponents)] ????(polynomial derivative-coefs x))) ?? (def f (partial polynomial [2 1 3])) ; 2x^2 + x + 3 (def f-prime (partial derivative [2 1 3])) ; 4x + 1 ?? (println "f(2) =" (f 2)) ; -> 13.0 (println "f'(2) =" (f-prime 2)) ; -> 9.0 |
下面是另外一種做法 (Francesco Strino建議的).
%1 = a, %2 = b, result is ax + b
%1 = ax + b, %2 = c, result is (ax + b)x + c = ax^2 + bx + c
| 1 2 3 4 5 | (defn- polynomial ??"computes the value of a polynomial ???with the given coefficients for a given value x" ??[coefs x] ??(reduce #(+ (* x %1) %2) coefs)) |
memoize 函數接受一個參數,它的作用就是給原來的函數加一個緩存,所以如果同樣的參數被調用了兩次, 那么它就直接從緩存里面返回緩存了的結果,以提高效率, 但是當然它會需要更多的內存。(其實也只有函數式編程里面能用這個技術, 因為函數沒有side-effect, 多次調用的結果保證是一樣的)
time 宏可以看成一個wrapper函數, 它會打印被它包起來的函數的執行時間,并且返回這個函數的返回值。看下面例子里面是怎么用的。
下面的例子演示在多項式的的計算里面使用memoize:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 | ; Note the use of def instead of defn because memoize returns ; a function that is then bound to "memo-f". (def memo-f (memoize f)) ?? (println "priming call") (time (f 2)) ?? (println "without memoization") ; Note the use of an underscore for the binding that isn't used. (dotimes [_ 3] (time (f 2))) ?? (println "with memoization") (dotimes [_ 3] (time (memo-f 2))) |
上面代碼的輸出是這樣的:
幫助| 01 02 03 04 05 06 07 08 09 10 | priming call "Elapsed time: 4.128 msecs" without memoization "Elapsed time: 0.172 msecs" "Elapsed time: 0.365 msecs" "Elapsed time: 0.19 msecs" with memoization "Elapsed time: 0.241 msecs" "Elapsed time: 0.033 msecs" "Elapsed time: 0.019 msecs" |
從上面的輸出我們可以看到好幾個東西。首先第一個方法調用比其它的都要長很多。– 其實這和用不用memonize沒有什么關系。第一個memoize調用所花的時間也要比其他memoize調用花的時間要長, 因為要操作緩存,其它的memoize調用就要快很多了。
回到上面
和Java的互操作
Clojure程序可以使用所有的java類以及接口。和在java里面一樣java.lang 這個包里面的類是默認導入的。你可以手動的用import 函數來導入其它包的類。看例子:
幫助| 1 2 3 | (import ??'(java.util Calendar GregorianCalendar) ??'(javax.swing JFrame JLabel)) |
同時也可以看下宏ns下面的:import 指令, 我們會在后面介紹的。
有兩種方式可以訪問類里面的常量的:
幫助| 1 2 3 4 | (. java.util.Calendar APRIL) ; -> 3 (. Calendar APRIL) ; works if the Calendar class was imported java.util.Calendar/APRIL Calendar/APRIL ; works if the Calendar class was imported |
在Clojure代碼里面調用java的方法是很簡單的。因此很多java里面已經實現的功能Clojure就沒有實現自己的了。比如, Clojure里面沒有提供函數來計算一個數的絕對值,因為可以用java.lang.Math 里面的abs方法。而另一方面,比如這個類里面還提供了一個max 方法來計算兩個數里面比較大的一個, 但是它只接受兩個參數,因此Clojure里面自己提供了一個可以接受多個參數的max函數。
有兩種方法可以調用java里面的靜態方法:
幫助| 1 2 | (. Math pow 2 4) ; -> 16.0 (Math/pow 2 4) |
同樣也有兩種方法來創建一個新的java的對象,看下面的例子。這里注意一下我們用def 創建的對象bind到一個全局的binding。這個其實不是必須的。有好幾種方式可以得到一個對象的引用比如把它加入一個集合或者把它傳入一個函數。
幫助| 1 2 3 | (import '(java.util Calendar GregorianCalendar)) (def calendar (new GregorianCalendar 2008 Calendar/APRIL 16)) ; April 16, 2008 (def calendar (GregorianCalendar. 2008 Calendar/APRIL 16)) |
同樣也有兩種方法可以調用java對象的方法:
幫助| 1 2 3 4 | (. calendar add Calendar/MONTH 2) (. calendar get Calendar/MONTH) ; -> 5 (.add calendar Calendar/MONTH 2) (.get calendar Calendar/MONTH) ; -> 7 |
一般來說我們比較推薦使用下面那種用法(.add, .get), 上面那種用法在定義宏的時候用得比較多, 這個等到我們講到宏的時候再做詳細介紹。
方法調用可以用.. 宏串起來:
幫助| 1 2 | (. (. calendar getTimeZone) getDisplayName) ; long way (.. calendar getTimeZone getDisplayName) ; -> "Central Standard Time" |
還一個宏:.?. 在clojure.contrib.core 名字空間里面, 它和上面..這個宏的區別是,在調用的過程中如果有一個返回結果是nil, 它就不再繼續調用了,可以防止出現NullPointerException異常。
doto 函數可以用來調用一個對象上的多個方法。它返回它的第一個參數, 也就是所要調用方法的對象。這對于初始化一個對象的對各屬性是非常方便的。 (看下面”Namespaces“那一節的JFrame GUI 對象的例子). 比如:
幫助| 1 2 3 4 5 6 | (doto calendar ??(.set Calendar/YEAR 1981) ??(.set Calendar/MONTH Calendar/AUGUST) ??(.set Calendar/DATE 1)) (def formatter (java.text.DateFormat/getDateInstance)) (.format formatter (.getTime calendar)) ; -> "Aug 1, 1981" |
memfn 宏可以自動生成代碼以使得java方法可以當成clojure里面的“一等公民”來對待。這個可以用來替代clojure里面的匿名方法。當用memfn 來調用java里面那些需要參數的方法的時候, 你必須給每個參數指定一個名字,以讓clojure知道你要調用的方法需要幾個參數。這些名字到底是什么不重要,但是它們必須要是唯一的, 因為要用這些名字來生成Clojure代碼的。下面的代碼用了一個map方法來從第二個集合里面取beginIndex來作為參數調用第一個集合里面的字符串的substring方法。大家可以看一下用匿名函數和用memfn來直接調用java的方法的區別。
幫助| 1 2 3 4 5 | (println (map #(.substring %1 %2) ???????????["Moe" "Larry" "Curly"] [1 2 3])) ; -> (oe rry ly) ?? (println (map (memfn substring beginIndex) ???????????["Moe" "Larry" "Curly"] [1 2 3])) ; -> same |
Proxies
proxy 創建一個繼承了指定類并且/或者實現了0個或者多個接口的類的對象。這對于創建那種必須要實現某個接口才能得到通知的listener對象很有用。舉一個例子, 大家可以看下面 “Desktop Applications” 那一節的例子。那里我們創建了一個繼承JFrame類并且實現ActionListener接口的類的對象。
Threads
所有的Clojure方法都實現了java.lang.Runnable 接口和java.util.concurrent.Callable 接口。這使得非常容易把Clojure里面函數和java里面的線程一起使用。比如:
幫助| 01 02 03 04 05 06 07 08 09 10 11 | (defn delayed-print [ms text] ??(Thread/sleep ms) ??(println text)) ?? ; Pass an anonymous function that invokes delayed-print ; to the Thread constructor so the delayed-print function ; executes inside the Thread instead of ; while the Thread object is being created. (.start (Thread. #(delayed-print 1000 ", World!"))) ; prints 2nd (print "Hello") ; prints 1st ; output is "Hello, World!" |
異常處理
Clojure代碼里面拋出來的異常都是運行時異常。當然從Clojure代碼里面調用的java代碼還是可能拋出那種需要檢查的異常的。try,catch,finally 以及throw 提供了和java里面類似的功能:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | (defn collection? [obj] ??(println "obj is a" (class obj)) ??; Clojure collections implement clojure.lang.IPersistentCollection. ??(or (coll? obj) ; Clojure collection? ??????(instance? java.util.Collection obj))) ; Java collection? ?? (defn average [coll] ??(when-not (collection? coll) ????(throw (IllegalArgumentException. "expected a collection"))) ??(when (empty? coll) ????(throw (IllegalArgumentException. "collection is empty"))) ??; Apply the + function to all the items in coll, ??; then divide by the number of items in it. ??(let [sum (apply + coll)] ????(/ sum (count coll)))) ?? (try ??(println "list average =" (average '(2 3))) ; result is a clojure.lang.Ratio object ??(println "vector average =" (average [2 3])) ; same ??(println "set average =" (average #{2 3})) ; same ??(let [al (java.util.ArrayList.)] ????(doto al (.add 2) (.add 3)) ????(println "ArrayList average =" (average al))) ; same ??(println "string average =" (average "1 2 3 4")) ; illegal argument ??(catch IllegalArgumentException e ????(println e) ????;(.printStackTrace e) ; if a stack trace is desired ??) ??(finally ????(println "in finally"))) |
上面代碼的輸出是這樣的:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 | obj is a clojure.lang.PersistentList list average = 5/2 obj is a clojure.lang.LazilyPersistentVector vector average = 5/2 obj is a clojure.lang.PersistentHashSet set average = 5/2 obj is a java.util.ArrayList ArrayList average = 5/2 obj is a java.lang.String #<;IllegalArgumentException java.lang.IllegalArgumentException: expected a collection> in finally |
回到上面
條件處理
if 這個special form跟java里面的if的語義是一樣的, 它接受三個參數, 第一個是需要判斷的條件,第二個表達式是條件成立的時候要執行的表達式,第三個參數是可選的,在條件不成立的時候執行。如果需要執行多個表達式,那么把多個表達式包在do里面。看例子:
幫助| 1 2 3 4 5 6 7 8 | (import '(java.util Calendar GregorianCalendar)) (let [gc (GregorianCalendar.) ??????day-of-week (.get gc Calendar/DAY_OF_WEEK) ??????is-weekend (or (= day-of-week Calendar/SATURDAY) (= day-of-week Calendar/SUNDAY))] ??(if is-weekend ????(println "play") ????(do (println "work") ????????(println "sleep")))) |
宏when 和when-not 提供和if類似的功能, 只是它們只在條件成立(或者不成立)時候執行一個表達式。另一個不同是,你可以執行任意數目的表達式而不用用do把他們包起來。
幫助| 1 2 | (when is-weekend (println "play")) (when-not is-weekend (println "work") (println "sleep")) |
宏if-let 把一個值bind到一個變量,然后根據這個binding的值來決定到底執行哪個表達式。下面的代碼會打印隊列里面第一個等待的人的名字,或者打印“no waiting”如果隊列里面沒有人的話。
幫助| 1 2 3 4 5 6 7 | (defn process-next [waiting-line] ??(if-let [name (first waiting-line)] ????(println name "is next") ????(println "no waiting"))) ?? (process-next '("Jeremy" "Amanda" "Tami")) ; -> Jeremy is next (process-next '()) ; -> no waiting |
when-let 宏跟if-let 類似, 不同之處跟上面if 和when的不同之處是類似的。 他們沒有else部分,同時還支持執行任意多個表達式。比如:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | (defn summarize ??"prints the first item in a collection ??followed by a period for each remaining item" ??[coll] ??; Execute the when-let body only if the collection isn't empty. ??(when-let [head (first coll)] ????(print head) ????; Below, dec subtracts one (decrements) from ????; the number of items in the collection. ????(dotimes [_ (dec (count coll))] (print \.)) ????(println))) ?? (summarize ["Moe" "Larry" "Curly"]) ; -> Moe.. (summarize []) ; -> no output |
condp 宏跟其他語言里面的switch/case語句差不多。它接受兩個參數,一個謂詞參數 (通常是= 或者instance?) 以及一個表達式作為第二個參數。在這之后,它接受任意數量的值-表達式的對子,這些對子會按順序evaluate。如果謂詞的條件跟某個值匹配了, 那么對應的表達式就被執行。一個可選的最后一個參數可以指定, 這個參數指定如果一個條件都不符合的話, 那么就返回這個值。如果這個值沒有指定,而且沒有一個條件符合謂詞, 那么一個IllegalArgumentException 異常就會被拋出。
下面的例子讓用戶輸入一個數字,如果用戶輸入的數字是1,2,3,那么程序會打印這些數字對應的英文單詞。否則它會打印”unexpected value”。在那之后,它會測試一個本地binding的類型,如果是個數字它會打印這個數字乘以2的結果;如果是字符串, 那么打印這個字符串的長度乘以2的結果。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | (print "Enter a number: ") (flush) ; stays in a buffer otherwise (let [reader (java.io.BufferedReader. *in*) ; stdin ??????line (.readLine reader) ??????value (try ??????????????(Integer/parseInt line) ??????????????(catch NumberFormatException e line))] ; use string value if not integer ??(println ????(condp = value ??????1 "one" ??????2 "two" ??????3 "three" ??????(str "unexpected value, \"" value \"))) ??(println ????(condp instance? value ??????Number (* value 2) ??????String (* (count value) 2)))) |
cond 宏接受任意個 謂詞/結果表達式 的組合。它按照順序來測試所有的謂詞,直到有一個謂詞的測試結果是true, 那么它返回其所對應的結果。如果沒有一個謂詞的測試結果是true, 那么會拋出一個IllegalArgumentException 異常。通常最后一個謂詞一般都是true, 以充當默認情況。
下面的例子讓用戶輸入水的溫度, 然后打印出水的狀態: 是凍住了,還是燒開了,還是一般狀態。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 | (print "Enter water temperature in Celsius: ") (flush) (let [reader (java.io.BufferedReader. *in*) ??????line (.readLine reader) ??????temperature (try ????????(Float/parseFloat line) ????????(catch NumberFormatException e line))] ; use string value if not float ??(println ????(cond ??????(instance? String temperature) "invalid temperature" ??????(<= temperature 0) "freezing" ??????(>= temperature 100) "boiling" ??????true "neither"))) |
回到上面
迭代
有很多方法可以遍歷一個集合。
宏dotimes 會執行給定的表達式一定次數, 一個本地binding會被給定值:從0到一個給定的數值. 如果這個本地binding是不需要的 (下面例子里面的card-number ), 可以用下劃線來代替, 看例子:
幫助| 1 2 | (dotimes [card-number 3] ??(println "deal card number" (inc card-number))) ; adds one to card-number |
注意下上面例子里面的inc 函數是為了讓輸出變成 1, 2, 3 而不是 0, 1, 2。上面代碼的輸出是這樣的:
幫助| 1 2 3 | deal card number 1 deal card number 2 deal card number 3 |
宏while 會一直執行一個表達式只要指定的條件為true. 下面例子里面的while 會一直執行,只要這個線程沒有停:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 | (defn my-fn [ms] ??(println "entered my-fn") ??(Thread/sleep ms) ??(println "leaving my-fn")) ?? (let [thread (Thread. #(my-fn 1))] ??(.start thread) ??(println "started thread") ??(while (.isAlive thread) ????(print ".") ????(flush)) ??(println "thread stopped")) |
上面代碼的輸出是這樣的:
幫助| 1 2 3 4 | started thread .....entered my-fn. .............leaving my-fn. thread stopped |
List Comprehension
宏for 和doseq 可以用來做list comprehension. 它們支持遍歷多個集合 (最右邊的最快) ,同時還可以做一些過濾用:when 和 :while。 宏for 只接受一個表達式 , 它返回一個懶惰集合作為結果. 宏doseq 接受任意數量的表達式, 以有副作用的方式執行它們, 并且返回nil.
下面的例子會打印一個矩陣里面 所有的元素出來。 它們會跳過 “B” 列 并且只輸出小于3的那些行。我們會在“序列”那一節介紹dorun , 它會強制提取 for 所返回的懶惰集合.
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 | (def cols "ABCD") (def rows (range 1 4)) ; purposely larger than needed to demonstrate :while ?? (println "for demo") (dorun ??(for [col cols :when (not= col \B) ????????row rows :while (< row 3)] ????(println (str col row)))) ?? (println "\ndoseq demo") (doseq [col cols :when (not= col \B) ????????row rows :while (< row 3)] ??(println (str col row))) |
上面的代碼的輸出是這樣的:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | for demo A1 A2 C1 C2 D1 D2 ?? doseq demo A1 A2 C1 C2 D1 D2 |
宏loop 是一個special form, 從它的名字你就可以猜出來它是用來遍歷的. 它以及和它類似的recur 會在下一節介紹.
回到上面
遞歸
遞歸發生在一個函數直接或者間接調用自己的時候。一般來說遞歸的退出條件有檢查一個集合是否為空,或者一個狀態變量是否變成了某個特定的值(比如0)。這一種情況一般利用連續調用集合里面的next 函數來實現。后一種情況一般是利用dec 函數來遞減某一個變量來實現。
如果遞歸的層次太深的話,那么可能會產生內存不足的情況。所以一些編程語言利用 “tail call optimization” (TCO)的技術來解決這個問題。但是目前Java和Clojure都不支持這個技術。在Clojure里面避免這個問題的一個辦法是使用special form:loop 和recur。另一個方法是使用trampoline 函數。
loop/recur 組合把一個看似遞歸的調用變成一個迭代 — 迭代不需要占用棧空間。 loop special form 跟let special form 類似的地方是它們都會建立一個本地binding,但是同時它也建立一個遞歸點, 而這個遞歸點就是recur的參數里面的那個函數。loop給這些binding一個初始值。對recur 的調用使得程序的控制權返回給loop 并且給那些本地binding賦了新的值。給recur傳遞的參數一定要和loop所創建的binding的個數一樣。同樣recur只能出現在loop這個special form的最后一行。
幫助| 1 2 3 4 5 6 7 8 9 | (defn factorial-1 [number] ??"computes the factorial of a positive integer ???in a way that doesn't consume stack space" ??(loop [n number factorial 1] ????(if (zero? n) ??????factorial ??????(recur (dec n) (* factorial n))))) ?? (println (time (factorial-1 5))) ; -> "Elapsed time: 0.071 msecs"\n120 |
defn 宏跟loop special form一樣也會建立一個遞歸點。 recur special form 也可以被用在一個函數的最后一句用來把控制權返回到函數的第一句并以新的參數重新執行。
另外一種實現 factorial 函數的方法是使用reduce 函數。這個我們在 “集合” 那一節就已經介紹過了。它支持一種更加“函數”的方式來做這個事情。不過不幸的是,在這種情況下,它的效率要低一點。注意一下range 函數返回一個數字的范圍, 這個范圍包括它的左邊界,但是不包括它的右邊界。
幫助| 1 2 3 | (defn factorial-2 [number] (reduce * (range 2 (inc number)))) ?? (println (time (factorial-2 5))) ; -> "Elapsed time: 0.335 msecs"\n120 |
你可以把上面的reduce 換成apply, 可以得到同樣的結果, 但是apply要更慢一點。這也說明了我們要熟悉每個方法的特點的重要性,以在各個場合使用合適的函數。
recur 不支持那種一個函數調用另外一個函數,然后那個函數再回調這個函數的這種遞歸。但是我們沒有提到的trampoline函數是支持的。
回到上面
謂詞
Clojure 提供了很多函數來充當謂詞的功能 — 測試條件是否成立。它們的返回值是 true或者false。在Clojure里面false 以及nil 被解釋成false. true 以及任何其他值都被解釋成true, 包括0。謂詞函數的名字一般以問號結尾。
反射是一種獲取一個對象的特性,而不是它的值的過程。比如說對象的類型。有很多謂詞函數進行反射。 測試一個對象的類型的謂詞包括class?,coll?,decimal?,delay?,float?,fn?,instance?,integer?,isa?,keyword?,list?,macro?,map?,number?,seq?,set?,string? 以及vector?。 一些非謂詞函數也進行反射操作,包括:ancestors,bases,class,ns-publics 以及parents。
測試兩個值之間關系的謂詞有:<,<=,=,not=,==,>,>=,compare,distinct? 以及identical?.
測試邏輯關系的謂詞有:and,or,not,true?,false? 和nil?
測試集合的一些謂詞在前面已經討論過了,包括:empty?,not-empty,every?,not-every?,some? 以及not-any?.
測試數字的謂詞有even?,neg?,odd?,pos? 以及zero?.
回到上面
序列
序列可以看成是集合的一個邏輯視圖。許多事物可以看成是序列。包括Java的集合,Clojure提供的集合,字符串,流,目錄結構以及XML樹。
很多Clojure的函數返回一個lazy序列(LazySeq), 這種序列里面的元素不是實際的數據, 而是一些方法, 它們直到用戶真正需要數據的時候才會被調用。LazySeq的一個好處是在你創建這個序列的時候你不用太擔心這個序列到底會有多少元素。下面是會返回lazySeq的一些函數:cache-seq,concat,cycle,distinct,drop,drop-last,drop-while,filter,for,interleave,interpose,iterate,lazy-cat,lazy-seq,line-seq,map,partition,range,re-seq,remove,repeat,replicate,take,take-nth,take-while andtree-seq。
LazySeq是剛接觸Clojure的人比較容易弄不清楚的一個東西。比如你們覺得下面這個代碼的輸出是什么?
幫助| 1 | (map #(println %) [1 2 3]) |
當在一個REPL里面運行的時候,它會輸出 1, 2 和 3 在單獨的行上面, 以及三個nil(三個println的返回結果)。REPL總是立即解析/調用我們所輸入的所有的表達式。但是當作為一個腳本來運行的時候,這句代碼不會輸出任何東西。因為map 函數返回的是一個LazySeq。
有很多方法可以強制LazySeq對它里面的方法進行調用。比如從序列里面獲取一個元素的方法first,second,nth 以及last 都能達到這個效果。序列里面的方法是按順序調用的, 所以你如果要獲取最后一個元素, 那么整個LazySeq里面的方法都會被調用。
如果LazySeq的頭被存在一個binding里面,那么一旦一個元素的方法被調用了, 那么這個元素的值會被緩存起來, 下次我們再來獲取這個元素的時候就不用再調用函數了。
dorun 和doall 函數迫使一個LazySeq里面的函數被調用。 doseq 宏, 我們在 "迭代" 那一節提到過的, 會迫使一個或者多個LazySeq里面的函數調用。for 宏, 也在是"迭代”那一節提到的,不會強制調用LazySeq里面的方法, 相反, 他會返回另外一個LazySeq。
為了只是簡單的想要迫使LazySeq里面的方法被調用,那么doseq 或者dorun 就夠了。調用的結果不會被保留的, 所以占用的內存也就比較少。這兩個方法的返回值都是nil. 如果你想調用的結果被緩存, 那么你應該使用doall.
下面的表格列出來了強制LazySeq里面的方法被調用的幾個辦法。
| doall | dorun |
| N/A | doseq |
一般來說我們比較推薦使用doseq 而不是dorun 函數, 因為這樣代碼更加易懂。 同時代碼效率也更高, 因為dorun內部使用map又創建了另外一個序列。比如下面的兩會的結果是一樣的。
幫助| 1 2 | (dorun (map #(println %) [1 2 3])) (doseq [i [1 2 3]] (println i)) |
如果一個方法會返回一個LazySeq并且在它的方法被調用的時候還會有副作用,那么大多數情況下我們應該使用doall 來調用并且返回它的結果。這使得副作用的出現時間更容易確定。否則的話別的調用者可能會調用這個LazySeq多次,那么副作用也就會出現多次 -- 從而可能出現錯誤的結果。
下面的幾個表達式都會在不同的行輸出1, 2, 3, 但是它們的返回值是不一樣的。do special form 是用來實現一個匿名函數,這個函數先打印這個值, 然后再把這個值返回。
幫助| 1 2 3 | (doseq [item [1 2 3]] (println item)) ; -> nil (dorun (map #(println %) [1 2 3])) ; -> nil (doall (map #(do (println %) %) [1 2 3])) ; -> (1 2 3) |
LazySeq使得創建無限序列成為可能。因為只有需要使用的數據才會在用到的時候被調用創建。比如
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | (defn f ??"square the argument and divide by 2" ??[x] ??(println "calculating f of" x) ??(/ (* x x) 2.0)) ?? ; Create an infinite sequence of results from the function f ; for the values 0 through infinity. ; Note that the head of this sequence is being held in the binding "f-seq". ; This will cause the values of all evaluated items to be cached. (def f-seq (map f (iterate inc 0))) ?? ; Force evaluation of the first item in the infinite sequence, (f 0). (println "first is" (first f-seq)) ; -> 0.0 ?? ; Force evaluation of the first three items in the infinite sequence. ; Since the (f 0) has already been evaluated, ; only (f 1) and (f 2) will be evaluated. (doall (take 3 f-seq)) ?? (println (nth f-seq 2)) ; uses cached result -> 2.0 |
下面的代碼和上面的代碼不一樣的地方是, 在下面的代碼里面LazySeq的頭沒有被保持在一個binding里面, 所以被調用過的方法的返回值不會被緩存。所以它所需要的內存比較少, 但是如果同一個元素被請求多次, 那么它的效率會低一點。
幫助| 1 2 3 | (defn f-seq [] (map f (iterate inc 0))) (println (first (f-seq))) ; evaluates (f 0), but doesn't cache result (println (nth (f-seq) 2)) ; evaluates (f 0), (f 1) and (f 2) |
另外一種避免保持LazySeq的頭的辦法是把這個LazySeq直接傳給函數:
幫助| 1 2 3 4 5 6 7 | (defn consumer [seq] ??; Since seq is a local binding, the evaluated items in it ??; are cached while in this function and then garbage collected. ??(println (first seq)) ; evaluates (f 0) ??(println (nth seq 2))) ; evaluates (f 1) and (f 2) ?? (consumer (map f (iterate inc 0))) |
回到上面
輸入輸出
Clojure提供了很少的方法來進行輸入/輸出的操作。因為我們在Clojure代碼里面可以很輕松的使用java里面的I/O操作方法。但是?clojure.java.io 庫使得使用java的I/O方法更加簡單。
這些預定義的special symbols*in*,*out* 以及*err* 默認被設定成 stdin, stdout 以及 stderr 。如果要flush*out*,里面的輸出,使用 (flush)方法,效果和(.flush *out*)一樣。當然這些symbol的binding是可以改變的。比如你可以把輸出重定向到 "my.log"文件里面去。 看下面的例子:
幫助| 1 2 3 4 5 | (binding [*out* (java.io.FileWriter. "my.log")] ??... ??(println "This goes to the file my.log.") ??... ??(flush)) |
print 可以打印任何對象的字符串表示到*out*,并且在兩個對象之間加一個空格。
println 函數和print類似, 但是它會在最后加一個newline符號。默認的話它還會有一個flush的動作。這個默認動作可以通過把 special symbol*flush-on-newline* 設成false來取消掉。
newline 函數寫一個newline符號*out* 流里面去。 在調用print 函數后面手動調用newline 和直接調用println的效果是一樣的。
pr 與prn 是和print 與println 想對應的一對函數, 但是他們輸出的形式可以被 Clojure reader去讀取。它們對于把Clojure的對象進行序列化的時候比較有用。默認情況下它們不會打印數據的元數據。可以通過把 special symbol*print-meta* 設置成true來調整這個行為。
下面的例子演示了我們提到的四個打印方法。注意使用print和pr輸出的字符串的不同之處。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 | (let [obj1 "foo" ??????obj2 {:letter \a :number (Math/PI)}] ; a map ??(println "Output from print:") ??(print obj1 obj2) ?? ??(println "Output from println:") ??(println obj1 obj2) ?? ??(println "Output from pr:") ??(pr obj1 obj2) ?? ??(println "Output from prn:") ??(prn obj1 obj2)) |
上面代碼的輸出是這樣的:
幫助| 1 2 3 4 5 6 | Output from print: foo {:letter a, :number 3.141592653589793}Output from println: foo {:letter a, :number 3.141592653589793} Output from pr: "foo" {:letter \a, :number 3.141592653589793}Output from prn: "foo" {:letter \a, :number 3.141592653589793} |
所有上面討論的幾個打印函數都會在它們的參數之間加一個空格。你可以通過str 函數來預先組裝好要打印的字符串來避免這個行為, 看下面例子:
幫助| 1 2 | (println "foo" 19) ; -> foo 19 (println (str "foo" 19)) ; -> foo19 |
print-str,println-str,pr-str 以及prn-str 函數print,println,pr 跟prn 類似, 只是它們返回一個字符串,而不是把他們打印出來。
printf 函數和print 類似。但是它接受一個format字符串。format 函數和printf, 類似,只是它是返回一個字符串而不是打印出來。
宏with-out-str 把它的方法體里面的所有輸出匯總到一個字符串里面并且返回。
with-open 可以自動關閉所關聯的連接(.close)方法, 這對于那種像文件啊,數據庫連接啊,比較有用,它有點像C#里面的using語句。
line-seq 接受一個java.io.BufferedReader 參數,并且返回一個LazySeq, 這個LazySeq包含所有的一行一行由BufferedReader讀出的文本。返回一個LazySeq的好處在于,它不用馬上讀出文件的所有的內容, 這會占用太大的內存。相反, 它只需要在需要使用的時候每次讀一行出來即可。
下面的例子演示了with-open 和line-seq的用法。 它讀出一個文件里面所有的行, 并且打印出包含某個關鍵字的那些行。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 | (use '1) ?? (defn print-if-contains [line word] ??(when (.contains line word) (println line))) ?? (let [file "story.txt" ??????word "fur"] ?? ??; with-open will close the reader after ??; evaluating all the expressions in its body. ??(with-open [rdr (reader file)] ????(doseq [line (line-seq rdr)] (print-if-contains line word)))) |
slurp 函數把一個文件里面的所有的內容讀進一個字符串里面并且返回。 spit 把一個字符串寫進一個文件里面然后關閉這個文件。
這篇文章只是大概過了一下clojure的io里面提供了哪些函數來進行I/O操作。大家可以看下clojure源文件:clojure/java/io.clj 以了解其它一些函數。
回到上面
解構
解構可以用在一個函數或者宏的參數里面來把一個集合里面的一個或者幾個元素抽取到一些本地binding里面去。它可以用在由let special form 或者binding 宏所創建的binding里面。
比如,如果我們有一個vector或者一個list, 我們想要獲取這個集合里面的第一個元素和第三個元素的和。那么可以用下面兩種辦法, 第二種解構的方法看起來要簡單一點。
幫助| 01 02 03 04 05 06 07 08 09 10 11 | (defn approach1 [numbers] ??(let [n1 (first numbers) ????????n3 (nth numbers 2)] ????(+ n1 n3))) ?? ; Note the underscore used to represent the ; second item in the collection which isn't used. (defn approach2 [[n1 _ n3]] (+ n1 n3)) ?? (approach1 [4 5 6 7]) ; -> 10 (approach2 [4 5 6 7]) ; -> 10 |
&符合可以在解構里面用來獲取集合里面剩下的元素。比如:
幫助| 1 2 3 4 | (defn name-summary [[name1 name2 & others]] ??(println (str name1 ", " name2) "and" (count others) "others")) ?? (name-summary ["Moe" "Larry" "Curly" "Shemp"]) ; -> Moe, Larry and 2 others |
:as 關鍵字可以用來獲取對于整個被解構的集合的訪問。如果我們想要一個函數接受一個集合作為參數,然后要計算它的第一個元素與第三個元素的和占總和的比例,看下面的代碼:
幫助| 1 2 3 4 | (defn first-and-third-percentage [[n1 _ n3 :as coll]] ??(/ (+ n1 n3) (apply + coll))) ?? (first-and-third-percentage [4 5 6 7]) ; ratio reduced from 10/22 -> 5/11 |
解構也可以用來從map里面獲取元素。假設我們有一個map這個map的key是月份, value對應的是這個月的銷售額。那么我們可以寫一個函數來計算夏季的總銷售額占全年銷售額的比例:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | (defn summer-sales-percentage ??; The keywords below indicate the keys whose values ??; should be extracted by destructuring. ??; The non-keywords are the local bindings ??; into which the values are placed. ??[{june :june july :july august :august :as all}] ??(let [summer-sales (+ june july august) ????????all-sales (apply + (vals all))] ????(/ summer-sales all-sales))) ?? (def sales { ??:january?? 100 :february 200 :march????? 0 :april??? 300 ??:may?????? 200 :june???? 100 :july???? 400 :august?? 500 ??:september 200 <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> ctober? 300 :november 400 :december 600}) ?? (summer-sales-percentage sales) ; ratio reduced from 1000/3300 -> 10/33 |
我們一般使用和map里面key的名字一樣的本地變量來對map進行解構,比如上面例子里面我們使用的{june :june july :july august :august :as all}. 這個可以使用:keys來簡化。比如,{:keys [june july august] :as all}.
回到上面
名字空間
Java用class來組織方法, 用包來組織class。Clojure用名字空間來組織事物。“事物”包括Vars, Refs, Atoms, Agents, 函數, 宏 以及名字空間本身。
符號(Symbols)是用來給函數、宏以及binding來分配名字的。符號被劃分到名字空間里面去了。 任何時候總有一個默認的名字空間,初始化的時候這個默認的名字空間是“user”,這個默認的名字空間的值被保存在特殊符號*ns*.里面。默認的名字空間可以通過兩種方法來改變。in-ns 函數只是改變它而已. 而ns 宏則做得更多。其中一件就是它會使得clojure.core 名字空間里面的符號在新的名字空間里面都可見 (使用refer 命令). ns 宏的其它一些特性我們會在后面介紹。
"user" 這個名字空間提供對于clojure.core 這個名字空間里面所有符號的訪問。同樣道理對于那些通過ns 宏來改變成默認名字空間的名字空間里面也是可以看到 clojure.core里面的所有的函數的。
如果要訪問哪些不在默認名字空間里面的符號、函數, 那么你必須要指定全限定的完整名字。比如 clojure.string 包里面定義了一個join 函數。它把多個字符串用一個分隔符隔開然后連起來,返回這個連起來的字符串。它的全限定名是clojure.string/join.
require 函數可以加載 Clojure 庫。它接受一個或者多一個名字空間的名字(注意前面的單引號)
幫助| 1 | (require 'clojure.string) |
這個只會加載這個類庫。這里面的名字還必須是一個全限定的報名, 包名之間用.分割。注意,clojure里面名字空間和方法名之間的分隔符是/而不是java里面使用的. 。比如:
幫助| 1 | (clojure.string/join "$" [1 2 3]) ; -> "1$2$3" |
alias 函數給一個名字空間指定一個別名以減少我們打字工作。當然這個別名的定義只在當前的名字空間里面有效。比如:
幫助| 1 2 | (alias 'su 'clojure.string) (su/join "$" [1 2 3]) ; -> "1$2$3" |
refer 函數使得指定的名字空間里面的函數在當前名字空間里面可以訪問(不用使用全限定名字)。一個特例就是如果當前名字空間有那個名字空間一樣的名字, 那么你訪問的時候還是要制定名字空間的。看例子:
幫助| 1 | (refer 'clojure.string) |
現在,上面的代碼可以寫成。
幫助| 1 | (join "$" [1 2 3]) ; -> "1$2$3" |
我們通常把require 和refer 結合使用, 所以clojure提供了一個use , 它相當于require和refer的簡潔形式。
幫助| 1 | (use 'clojure.string) |
我們前面提到過的 ns 宏, 可以改變當前的默認名字空間。我們通常在一個源代碼的最上面指定這個。它支持這些指令::require,:use和:import (用來加載 Java 類的) 這些其實是它們對應的函數的另外一種方式。我們鼓勵使用這些指令而不是那些函數。 在下面的例子里面 注意:as 給名字空間創建了一個別名。同時注意使用 nly 指令來加載Clojure庫的一部分。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | (ns com.ociweb.demo ??(:require 1) ??; assumes this dependency: [org.clojure/math.numeric-tower "0.0.1"] ??(:use 1) ??(:import (java.text NumberFormat) (javax.swing JFrame JLabel))) ?? (println (su/join "$" [1 2 3])) ; -> 1$2$3 (println (gcd 27 72)) ; -> 9 (println (sqrt 5)) ; -> 2.23606797749979 (println (.format (NumberFormat/getInstance) Math/PI)) ; -> 3.142 ?? ; See the screenshot that follows this code. (<a name="doto">doto</a> (JFrame. "Hello") ??(.add (JLabel. "Hello, World!")) ??(.pack) ??(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) ??(.setVisible true)) |
create-ns 函數可以創建一個新的名字空間。但是不會把它變成默認的名字空間。def 在當前名字定義一個符號,你同時還可以給它一個初始值。intern 函數在一個指定名字空間里面定義一個符號(如果這個符號不存在的話) , 同時還可以給它指定一個默認值。注意在intern里面符號的名字要括起來,但是在def里面不需要。這是因為def 是一個 special form, special form 不會evaluate它的參數, 而intern 是一個函數, 它會evaluate它的參數。看例子:
幫助| 1 2 3 4 | (def foo 1) (create-ns 'com.ociweb.demo) (intern 'com.ociweb.demo 'foo 2) (println (+ foo com.ociweb.demo/foo)) ; -> 3 |
ns-interns 函數返回一個指定的名字空間的所有的符號的map(這個名字空間一定要在當前名字空間里面加載了), 這個map的key是符號的名字, value是符號所對應的Var 對象, 這個對象表示的可能是函數,宏或者binding。 比如:
幫助| 1 | (ns-interns 'clojure.math.numeric-tower) |
all-ns 函數返回一個包含當前所有的已經加載了的名字空間的集合。下面這些名字空間是默認加載的:clojure.core,clojure.main,clojure.set,clojure.xml,clojure.zip 以及user. 而如果是在用REPL的話, 那么下面這些名字空間也會被加載:clojure.repl 和clojure.java.javadoc.
namespace 函數返回一個給定符號或者關鍵字的名字空間。
其它一些在這里沒有討論的名字空間相關的函數還包括ns-aliases,ns-imports,ns-map,ns-name,ns-publics,ns-refers,ns-unalias,ns-unmap 和remove-ns.
Some Fine Print
Symbol 對象有一個String 類型的名字以及一個String 類型的名字空間名字(叫做ns), 但是沒有值。它使用一個字符串的名字空間而不是一個名字空間對象使得它可以指向一個還不存在的名字空間。Var 對象有一個執行Symbol 對象的引用 (叫做sym), 一個指向Namespace對象的引用 (叫做ns) 以及一個Object 類型的對象(也就是它的root value, 叫做root). Namespace對象bjects有一個指向Map 的引用, 這個map維護Symbol 對象和Var 對象的對應關系 (叫做mappings)。同時它還有一個map來維護Symbol 別名和Namespace 對象之間的關系 (叫做namespaces). 下面這個類圖顯示了Java里面的類和接口在Clojure里面的實現。在Clojure里面 "interning" 這個單詞一般指的是添加一個Symbol到Var 的對應關系到一個Namespace里面去。
回到上面
元數據
Clojure里面的元數據是附加到一個符號或者集合的一些數據,它們和符號或者集合的邏輯數據沒有直接的關系。兩個邏輯上一樣的方法可以有不同的元數據。 下面是一個有關撲克牌的例子
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 | (defstruct card-struct :rank :suit) ?? (def card1 (struct card-struct :king :club)) (def card2 (struct card-struct :king :club)) ?? (println (== card1 card2)) ; same identity? -> false (println (= card1 card2)) ; same value? -> true ?? (def card2 #^{:bent true} card2) ; adds metadata at read-time (def card2 (with-meta card2 {:bent true})) ; adds metadata at run-time (println (meta card1)) ; -> nil (println (meta card2)) ; -> {:bent true} (println (= card1 card2)) ; still same value despite metadata diff. -> true |
一些元數據是Clojure內部定義的。比如:private 它表示一個Var是否能被包外的函數訪問。:doc 是一個 Var 的文檔字符串。:test 元數據是一個Boolean值表示這個函數是否是一個測試函數。
:tag 是一個字符串類型的類名或者一個Class 對象,表示一個Var在Java里面對應的類型,或者一個函數的返回值。這些被稱為“類型提示” 。提供這些可以提高代碼性能。如果你想查看你的clojure代碼里面哪里使用反射來決定類型信息 -- 也就是說這里可能會有性能的問題, 那么你可以設置全局變量*warn-on-reflection* 為true。
一些元數據會由Clojure的編譯器自動地綁定到Var對象。:file 是定義這個 Var的文件的名字。:line 是定義這個Var的行數。:name 是一個Var的名字的Symbol 對象。:ns 是一個Namespace 對象描述這個Var所在的名字空間。:macro 是一個標識符標識這個符號是不是一個宏。:arglist 是一個裝有一堆vector的一個list, 表示一個函數所接受的所有的參數列表(前面在介紹函數的時候說過一個函數可以接受多個參數列表)。
函數以及宏,都是有一個Var 對象來表示的, 它們都有關聯的元數據。比如輸入這個在REPL里面:(meta (var reverse)) 或者^#'reverse。輸出結果應該下面這些類似(為了好看我加了換行縮進)
幫助| 1 2 3 4 5 6 7 8 | { ??:ns #<;Namespace clojure.core>, ??:name reverse, ??:file "core.clj", ??:line 630, ??:arglists ([coll]), ??:doc "Returns a seq of the items in coll in reverse order. Not lazy." } |
clojure.repl包里面的source 函數, 利用元數據來獲取一個指定函數的源代碼,比如:
幫助| 1 | (source reverse) |
上面代碼的輸出應該是:
幫助| 1 2 3 4 | (defn reverse ??"Returns a seq of the items in coll in reverse order. Not lazy." ??[coll] ????(reduce conj nil coll)) |
回到上面
宏
宏是用來給語言添加新的結構,新的元素的。它們是一些在讀入期(而不是編譯期)就會實際代碼替換的一個機制。
對于函數來說,它們的所有的參數都會被evaluate的, 而宏則會自動判斷哪些參數需要evaluate。 這對于實現像(if condition then-expr else-expr)這樣的結構是非常重要的。 如果 condition 是true, 那么只有 "then" 表達式需要被evaluated. 如果條件是false, 那么只有 "else" 表達式應該被 evaluated. 這意味著if 不能被實現成一個函數 (它其實也不是宏, 而是一個special form)。其它一些因為這個原因而必須要實現成宏的包括and 和or 因為它們需要實現 "short-circuit"屬性。
要想知道一個東西到底是函數還是宏, 可以在REPL里面輸入(doc name) 或者查看它的元數據。如果是一個宏的話,那么它的元數據里面包含一個:macro key, 并且它的值為true。 比如,我們要看看and, 是不是宏, 在REPL里面輸入下面的命令:
幫助| 1 2 | ((meta (var and)) :macro) ; long way -> true (^#'and :macro) ; short way -> true |
讓我們通過一些例子來看看如何編寫并且使用宏。假設我們代碼里面很多地方要對一個數字進行判斷,通過判斷它是接近0, 是正的, 是負的來執行不同的邏輯;我們又不想這種判斷的代碼到處重復,那么這種情況下我們就可以使用宏了。我們使用defmacro 宏來定義一個宏。
幫助| 1 2 3 4 5 6 | (defmacro around-zero [number negative-expr zero-expr positive-expr] ??`(let [number# ~number] ; so number is only evaluated once ????(cond ??????(< (Math/abs number#) 1e-15) ~zero-expr ??????(pos? number#) ~positive-expr ??????true ~negative-expr))) |
Clojure的reader會把所有調用around-aero的地方全部換成defmacro這個方法體里面的具體代碼。我們在這里使用let是為了性能,因為這個傳進來的number是一個表達式而不是一個簡單的值, 而且被cond語句里面使用了兩次。自動產生的變量number#是為了產生一個不會和用戶指定的其它binding沖突的一個名字。這使得我們可以創建hygienic macros.
宏定義開始的時候的那個反引號 (也稱為語法引號) 防止宏體內的任何一個表達式被evaluate -- 除非你顯示地轉義了。這意味著宏體里面的代碼會原封不動地替換到使用這個宏的所有的地方 -- 除了以波浪號開始的那些表達式。 (number,zero-expr,positive-expr 和negative-expr). 當一個名字前面被加了一個波浪號,并且還在反引號里面,它的值會被替換的。如果這個名字代表的是一個序列,那么我們可以用~@ 這個語法來替換序列里面的某個具體元素。
下面是兩個使用這個宏的例子:(輸出都應該是 "+").
幫助| 1 2 | (around-zero 0.1 (println "-") (println "0") (println "+")) (println (around-zero 0.1 "-" "0" "+")) ; same thing |
如果對于每種條件執行多于一個表達式, 那么用do把他們包起來。看下面例子:
幫助| 1 2 3 4 | (around-zero 0.1 ??(do (log "really cold!") (println "-")) ??(println "0") ??(println "+")) |
為了驗證這個宏是否被正確展開, 在REPL里面輸入這個:
幫助| 1 2 | (macroexpand-1 ??'(around-zero 0.1 (println "-") (println "0") (println "+"))) |
它會輸出下面這個(為了容易看懂, 我加了縮進)
幫助| 1 2 3 4 5 | (clojure.core/let [number__3382__auto__ 0.1] ??(clojure.core/cond ????(clojure.core/<; (Math/abs number__3382__auto__) 1.0E-15) (println "0") ????(clojure.core/pos? number__3382__auto__) (println "+") ????true (println "-"))) |
下面是一個使用這個宏來返回一個描述輸入數字的屬性的字符串的函數。
幫助| 1 2 | (defn number-category [number] ??(around-zero number "negative" "zero" "positive")) |
下面是一些示例用法:
幫助| 1 2 3 | (println (number-category -0.1)) ; -> negative (println (number-category 0)) ; -> zero (println (number-category 0.1)) ; -> positive |
因為宏不會 evaluate 它們的參數, 所以你可以在宏體里面寫一個對函數的參數調用. 函數定義不能這么做,相反只能用匿名函數把它們包起來。
下面是一個接受兩個參數的宏。第一個是一個接受一個參數的函數, 這個參數是一個弧度, 如果它是一個三角函數sin, cos。第二個參數是一個弧度。如果這個被寫成一個函數而不是一個 宏的話, 那么我們需要傳遞一個#(Math/sin %) 而不是簡單的Math/sin 作為參數。注意 那些后面的#符號, 它會產生一個唯一的、不沖突的本地binding。# 和~ 都必須在反引號引著的列表里面才能使用。
幫助| 1 2 3 4 | (defmacro trig-y-category [fn degrees] ??`(let [radians# (Math/toRadians ~degrees) ?????????result# (~fn radians#)] ?????(number-category result#))) |
讓我們試一下。下面代碼的期望輸出應該是 "zero", "positive", "zero" 和 "negative".
幫助| 1 2 | (doseq [angle (range 0 360 90)] ; 0, 90, 180 and 270 ??(println (trig-y-category Math/sin angle))) |
宏的名字不能作為參數傳遞給函數。比如一個宏的名字比如and 不能作為參數傳遞給reduce函數。一個繞過的方法是定義一個匿名函數把這個宏包起來。比如(fn [x y] (and x y)) 或者#(and %1 %2). 宏會在這個讀入期在這個匿名函數體內解開。當這個函數被傳遞給函數比如reduce, 傳遞的是函數而不是宏。
宏的調用是在讀入期處理的。
回到上面
并發
Wikipedia上面對于并發有個很精準的定義:
"Concurrency is a property of systems in which several computations are executing and overlapping in time, and potentially interacting with each other. The overlapping computations may be executing on multiple cores in the same chip, preemptively time-shared threads on the same processor, or executed on physically separated processors."
并發編程的主要挑戰就在于管理對于共享的、可修改的狀態的修改。
通過鎖來對并發進行管理是非常難的。我們需要決定哪些對象需要加鎖以及什么時候加鎖。這還不算完, 每次你修改代碼或者添加新的代碼的時候你都要重新審視下你的這些決定。如果一個開發人員忘記了去鎖一個應該加鎖的對象,或者鎖的時機不對,一些非常糟糕的事情就會發生了。這些糟糕的事情包括死鎖和競爭條件;另一個方面如果你鎖了一個不需要鎖的對象,那么你的系統的性能則會下降。
為了更好地進行并發編程是很多開發人員選擇Clojure的原因。Clojure的所有的數據都是只讀的,除非你顯示的用Var,Ref,Atom 和Agent來標明它們是可以修改的。這些提供了安全的方法去管理共享狀態,我們會在下一節:“引用類型”里面更加詳細地介紹。
用一個新線程來運行一個Clojure函數是非常簡單的,不管它是內置的,還是自定義的, 不管它是有名的還是匿名的。關于這個更詳細的可以看上面有關和Java的互操作的討論。
因為Clojure代碼可以使用java里面的所有的類和接口, 所以它可以使用Java的并發能力。Java領域一個很棒的有關java并發編程的書: "Java Concurrency In Practice". 這本書里面講到了很多java里面如果做好并發編程的一些建議。但是要做到這些建議并不是一件很輕松的事情。在大多數情況下,使用java的引用類型比使用java里面并發要更簡單。
除了引用類型, Clojure還提供了其它一些函數來使你的并發編程更簡單。
future 宏把它的body里面的表達式在另外一個線程里面執行(這個線程來自于CachedThreadPool,Agents(后面會介紹)用的也是這個). 這個對于那種運行時間比較長, 而且一下子也不需要運行結果的程序來說比較有用。你可以通過dereferencing 從future. 放回的對象來得到返回值。 如果計算已經結束了, 那么立馬返回那個值;如果計算還沒有結束,那么當前線程會block住,直到計算結束返回。因為這里使用了一個來自Agent線程池的線程, 所以我們要在一個適當的時機調用shutdown-agents 關閉這些線程,然后程序才能退出。
為了演示future的用法, 我們加了一些println的方法調用,它能幫助我們觀察方法執行的狀態,注意輸出的消息的順序。
幫助| 1 2 3 4 5 | (println "creating future") (def my-future (future (f-prime 2))) ; f-prime is called in another thread (println "created future") (println "result is" @my-future) (shutdown-agents) |
如果f-prime 是一個比較耗時的方法的話, 那么輸出應該是這樣的:
幫助| 1 2 3 4 | creating future created future derivative entered result is 9.0 |
pmap 函數把一個函數作用到一個集合里面的所有的元素, 和map不一樣的是這個過程是完全并行的, 所以如果你要調用的這個函數是非常耗時間的話, 那么使用pmap將比使用
clojure.parallel 名字空間里買你有好多方法可以幫助你并行化你的代碼, 他們包括:par,pdistinct,pfilter-dupes,pfilter-nils,pmax,pmin,preduce,psort,psummary 和pvec.
回到上面
引用類型
引用類型是一種可變引用指向不可變數據的一種機制。Clojure里面有4種引用類型:Vars,Refs,Atoms 和Agents. 它們有一些共同的特征:
- 它們都可以指向任意類型的對象。
- 都可以利用函數deref 以及宏@ 來讀取它所指向的對象。
- 它們都支持驗證函數,這些函數在它們所指向的值發生變化的時候自動調用。如果新值是合法的值,那么驗證函數簡單的返回true, 如果新值是不合法的,那么要么返回false, 要么拋出一個異常。如果只是簡單地返回了false, 那么一個IllegalStateException 異常會被拋出,并且帶著提示信息: "Invalid reference state" 。
- 如果是Agents的話,它們還支持watchers。如果被監聽的引用的值發生了變化,那么Agent會得到通知, 詳情見 "Agents" 一節。
下面的這個表格總結了一下四種引用類型的區別,以及分別要用什么方法去創建或者修改它們。這個表格里面提到的函數我們會在后面介紹。
| 同步對于一個線程本地(thread-local)的變量的修改。 | 同步、協調對于一個或者多個值的修改 | 同步對于一個值的修改 | 對一個值進行異步修改 |
| (def name initial-value) | (ref initial-value) | (atom initial-value) | (agent initial-value) |
| (def name new-value) 可以賦新的值? (alter-var-root (set! name new-value) 在一個binding form 里滿設置一個新的、線程本地的值 | (ref-set ref new-value) 必須在dosync里面調用? (alter ref (commute ref | (reset! atom new-value)? (compare-and-set! atom current-value new-value) (swap! atom | (send agent update-fn arguments)? (send-off agent |
Vars
Vars 是一種可以有一個被所有線程共享的root binding并且每個線程線程還能有自己線程本地(thread-local)的值的一種引用類型。
下面的語法創建一個Var并且給它一個root binding:
幫助| 1 | (def <;em>name</em> <em>value</em>) |
你可以不給它一個值的。如果你沒有給它一個值,那么我們說這個Var是 "unbound". 同樣的語法可以用來修改一個Var的root binding。
有兩種方法可以創建一個已經存在的Var的線程本地binding(thread-local-binding):
幫助| 1 2 | (binding [<em>name</em> <em>expression</em>] <;em>body</em>) (set! <em>name</em> <em>expression</em>) ; inside a binding that bound the same name |
關于binding 宏的用法我們前面已經介紹過了. 下面的例子演示把它和set! 一起使用. 用set!來修改一個由binding bind的Var的線程本地的值。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | (def v 1) ?? (defn change-it [] ??(println "2) v =" v) ; -> 1 ?? ??(def v 2) ; changes root value ??(println "3) v =" v) ; -> 2 ?? ??(binding [v 3] ; binds a thread-local value ????(println "4) v =" v) ; -> 3 ?? ????(set! v 4) ; changes thread-local value ????(println "5) v =" v)) ; -> 4 ?? ??(println "6) v =" v)) ; thread-local value is gone now -> 2 ?? (println "1) v =" v) ; -> 1 ?? (let [thread (Thread. #(change-it))] ??(.start thread) ??(.join thread)) ; wait for thread to finish ?? (println "7) v =" v) ; -> 2 |
我們一般不鼓勵使用 Vars, 因為線程之間對于同一個Var的修改沒有做很好的協調,比如線程A在使用一個Var的root值,然后才發現,在它使用這個值的時候,已經有一個線程B在修改這個值了。
Refs
Refs是用來協調對于一個或者多個binding的并發修改的。這個協調機制是利用Software Transactional Memory (STM)來實現的。 Refs指定在一個事務里面修改。
STM在某些方面跟數據庫的事務很像。在一個STM事務里面做的修改只有在事務提交之后別的線程才能看到。這實現了ACID里面的A和I。Validation函數是的對Ref的修改與跟它相關的其它的值是一致的(consistent), 也就實現了C。
要想你的代碼在一個事務里面執行, 那么要把你的代碼包在宏dosync 的體內。當在一個事務里面對值進行修改,被改的其實是一個私有的、線程內的、直到事務提交才會被別的線程看到的一快內存。
如果到事務結束的時候也沒有異常拋出的話, 那么這個事務會順利的提交, 在事務里面所作的改變也就可以被別的線程看到了。
如果在事務里面有一個異常拋出,包括validation函數拋出的異常,那么這個事務會被回滾,事務里面對值做的修改也就會撤銷。
如果在一個事務里面,我們要對一個Ref進行修改,但是發現從我們的事務開始之后,已經有別的線程對這個Ref做了改動(沖突了), 那么當前事務里面的改動會被撤銷,然后從dosync的開頭重試。那到底什么時候會檢測到沖突, 什么時候會進行重試, 這個是沒有保證的, 唯一保證的是clojure為檢測到沖突,并且會進行重試。
要在事務里面執行的代碼一定要是沒有副作用的,這一點非常重要,因為前面提到的,事務可能會跟別的事務事務沖突,然后重試, 如果有副作用的話,那么出來的結果就不對了。不過要執行有副作用的代碼也是可能的, 可以把這個方法調用包裝給Agent, 然后這個方法會被hold住直到事務成功提交,然后執行一次。如果事務失敗那么就不會執行。
ref 函數可以創建一個 Ref 對象。下面的例子代碼創建一個Ref并且得到它的引用。
幫助| 1 | (def <;em>name</em> (ref <em>value</em>)) |
dosync 宏用來包裹一個事務 -- 從它對應的左括號開始,到它對應的右括號結束。在事務里面我們用ref-set 來改變一個Ref的值并且返回這個值。你不能在事務之外調用這個函數,否則會拋出IllegalStateException 異常。 看例子:
幫助| 1 2 3 4 | (dosync ??... ??(ref-set <;em>name</em> <em>new-value</em>) ??...) |
如果你要賦的新值是基于舊的值的話,那么就需要三個步驟了:
alter 和commute 函數在一個操作里面完成這三個步驟。 alter 函數是用來操作那些必須以特定順序進行的修改。而commute 函數則是要來操作那些修改順序不是很重要 -- 可以同時進行的修改。 跟ref-set, 一樣, 它們只能在一個事務里面調用。它們都接受一個 "update 函數" 做為參數, 以及一些額外的參數來計算新的值。這個函數會被傳遞這個Ref在線程內的當前的值以及一些額外的參數(如果有的話)。當我們要賦的新的值是基于舊的值計算出來的時候, 那么我們鼓勵使用alter 和commute 而不是ref-set.
比如,我們想給一個Ref:counter加一, 我們可以用inc 函數來實現:
幫助| 1 2 3 4 5 6 | (dosync ??... ??(alter counter inc) ??; or as ??(commute counter inc) ??...) |
如果alter 試圖修改的 Ref 在當前事務開始之后被別的事務改變了,那么當前事務會進行重試。而同樣的情況下commute 不會進行重試。它會以事務內的當前值進行計算。這會獲得比較好的性能(因為不進行重試)。但是要記住的是commute 函數只有在多個線程對Ref的修改順序不重要的時候才能使用。
如果一個事務提交了, 那么對于commute 函數還會有一些額外的事情發生。對于每一個commute 調用, Ref 的值會被下面的調用結果重置:
幫助| 1 | (apply <;em>update-function</em> <em>last-committed-value-of-ref</em> <em>args</em>) |
注意,這個update-function會被傳遞這個Ref最后被提交的值, 這個值可能是另外一個、在我們當前事務開始之后才開始的事務。
使用commute 而不是alter 是一種優化。只要對Ref進行更新的順序不會影響到這個Ref的最終的值。
然后看一個使用了 Refs 和 Atoms (后面會介紹)的例子。這個例子涉及到銀行賬戶以及賬戶之間的交易。首先我們定義一下數據模型。
幫助| 01 02 03 04 05 06 07 08 09 10 | (ns com.ociweb.bank) ?? ; Assume the only account data that can change is its balance. (defstruct account-struct :id <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> wner :balance-ref) ?? ; We need to be able to add and delete accounts to and from a map. ; We want it to be sorted so we can easily ; find the highest account number ; for the purpose of assigning the next one. (def account-map-ref (ref (sorted-map))) |
下面的函數建立一個新的帳戶,并且把它存入帳戶的map, ? 然后返回它。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | (defn open-account ??"creates a new account, stores it in the account map and returns it" ??[owner] ??(dosync ; required because a Ref is being changed ????(let [account-map @account-map-ref ??????????last-entry (last account-map) ??????????; The id for the new account is one higher than the last one. ??????????id (if last-entry (inc (key last-entry)) 1) ??????????; Create the new account with a zero starting balance. ??????????account (struct account-struct id owner (ref 0))] ??????; Add the new account to the map of accounts. ??????(alter account-map-ref assoc id account) ??????; Return the account that was just created. ??????account))) |
下面的函數支持從一個賬戶里面存/取錢。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | (defn deposit [account amount] ??"adds money to an account; can be a negative amount" ??(dosync ; required because a Ref is being changed ????(Thread/sleep 50) ; simulate a long-running operation ????(let [owner (account <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> wner) ??????????balance-ref (account :balance-ref) ??????????type (if (pos? amount) "deposit" "withdraw") ??????????direction (if (pos? amount) "to" "from") ??????????abs-amount (Math/abs amount)] ??????(if (>;= (+ @balance-ref amount) 0) ; sufficient balance? ????????(do ??????????(alter balance-ref + amount) ??????????(println (str type "ing") abs-amount direction owner)) ????????(throw (IllegalArgumentException. ?????????????????(str "insufficient balance for " owner ??????????????????????" to withdraw " abs-amount))))))) ?? (defn withdraw ??"removes money from an account" ??[account amount] ??; A withdrawal is like a negative deposit. ??(deposit account (- amount))) |
下面是函數支持把錢從一個賬戶轉到另外一個賬戶。由dosync 所開始的事務保證轉賬要么成功要么失敗,而不會出現中間狀態。
幫助| 1 2 3 4 5 6 7 | (defn transfer [from-account to-account amount] ??(dosync ????(println "transferring" amount ?????????????"from" (from-account <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> wner) ?????????????"to" (to-account <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> wner)) ????(withdraw from-account amount) ????(deposit to-account amount))) |
下面的函數支持查詢賬戶的狀態。由dosync 所開始的事務保證事務之間的一致性。比如把不會報告一個轉賬了一半的金額。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 | (defn- report-1 ; a private function ??"prints information about a single account" ??[account] ??; This assumes it is being called from within ??; the transaction started in report. ??(let [balance-ref (account :balance-ref)] ????(println "balance for" (account <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> wner) "is" @balance-ref))) ?? (defn report ??"prints information about any number of accounts" ??[& accounts] ??(dosync (doseq [account accounts] (report-1 account)))) |
上面的代碼沒有去處理線程啟動時候可能拋出的異常。相反,我們在當前線程給他們定義了一個異常處理器。
幫助| 1 2 3 4 5 6 7 | ; Set a default uncaught exception handler ; to handle exceptions not caught in other threads. (Thread/setDefaultUncaughtExceptionHandler ??(proxy [Thread$UncaughtExceptionHandler] [] ????(uncaughtException [thread throwable] ??????; Just print the message in the exception. ??????(println (.. throwable .getCause .getMessage))))) |
現在我們可以調用上面的函數了。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | (let [a1 (open-account "Mark") ??????a2 (open-account "Tami") ??????thread (Thread. #(transfer a1 a2 50))] ??(try ????(deposit a1 100) ????(deposit a2 200) ?? ????; There are sufficient funds in Mark's account at this point ????; to transfer $50 to Tami's account. ????(.start thread) ; will sleep in deposit function twice! ?? ????; Unfortunately, due to the time it takes to complete the transfer ????; (simulated with sleep calls), the next call will complete first. ????(withdraw a1 75) ?? ????; Now there are insufficient funds in Mark's account ????; to complete the transfer. ?? ????(.join thread) ; wait for thread to finish ????(report a1 a2) ????(catch IllegalArgumentException e ??????(println (.getMessage e) "in main thread")))) |
上面代碼的輸出是這樣的:
幫助| 1 2 3 4 5 6 7 8 | depositing 100 to Mark depositing 200 to Tami transferring 50 from Mark to Tami withdrawing 75 from Mark transferring 50 from Mark to Tami (a retry) insufficient balance for Mark to withdraw 50 balance for Mark is 25 balance for Tami is 200 |
Validation函數
在繼續介紹下一個引用類型之前,下面是一個validation函數的例子,他驗證所有賦給Ref的值是數字。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | ; Note the use of the :validator directive when creating the Ref ; to assign a validation function which is integer? in this case. (def my-ref (ref 0 :validator integer?)) ?? (try ??(dosync ????(ref-set my-ref 1) ; works ?? ????; The next line doesn't work, so the transaction is rolled back ????; and the previous change isn't committed. ????(ref-set my-ref "foo")) ??(catch IllegalStateException e ????; do nothing ????)) ?? (println "my-ref =" @my-ref) ; due to validation failure -> 0 |
Atoms
Atoms 提供了一種比使用Refs&STM更簡單的更新當個值的方法。它不受事務的影響
有三個函數可以修改一個Atom的值:reset!,compare-and-set! 和swap!.
reset! 函數接受兩個參數:要設值的Atom以及新值。它設置新的值,而不管你舊的值是什么。看例子:
幫助| 1 2 3 | (def my-atom (atom 1)) (reset! my-atom 2) (println @my-atom) ; -> 2 |
compare-and-set! 函數接受三個參數:要被修改的Atom, 上次讀取時候的值,新的值。 這個函數在設置新值之前會去讀Atom現在的值。如果與上次讀的時候的值相等, 那么設置新值并返回true, 否則不設置新值, 返回false。看例子:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | (def my-atom (atom 1)) ?? (defn update-atom [] ??(let [curr-val @my-atom] ????(println "update-atom: curr-val =" curr-val) ; -> 1 ????(Thread/sleep 50) ; give reset! time to run ????(println ??????(compare-and-set! my-atom curr-val (inc curr-val))))) ; -> false ?? (let [thread (Thread. #(update-atom))] ??(.start thread) ??(Thread/sleep 25) ; give thread time to call update-atom ??(reset! my-atom 3) ; happens after update-atom binds curr-val ??(.join thread)) ; wait for thread to finish ?? (println @my-atom) ; -> 3 |
為什么最后的結果是 3呢? update-atom 被放在一個單獨的線程里面,在reset! 函數調用之前執行。所以它獲取了atom的初始值1(存到變量curr-val里面去了), 然后它sleep了以讓reset! 函數有執行是時間。在那之后,atom的值就變成3了。當update-atom 函數調用compare-and-set! 來給這個值加一的時候, 它發現atom的值已經不是它上次讀取的那個值了(1), 所以更新失敗, atom的值還是3。
swap! 函數接受一個要修改的 Atom, 一個計算Atom新值的函數以及一些額外的參數(如果需要的話)。這個計算Atom新的值的函數會以這個Atom以及一些額外的參數做為輸入。swap!函數實際上是對compare-and-set!函數的一個封裝,但是有一個顯著的不同。 它首先把Atom的當前值存入一個變量,然后調用計算新值的函數來計算新值, 然后再調用compare-and-set!函數來賦值。如果賦值成功的話,那就結束了。如果賦值不成功的話, 那么它會重復這個過程,一直到賦值成功為止。這就是它們的區別:所以上面的代碼可以用swap!改寫成這樣:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | (def my-atom (atom 1)) ?? (defn update-atom [curr-val] ??(println "update-atom: curr-val =" curr-val) ??(Thread/sleep 50) ; give reset! time to run ??(inc curr-val)) ?? (let [thread (Thread. #(swap! my-atom update-atom))] ??(.start thread) ??(Thread/sleep 25) ; give swap! time to call update-atom ??(reset! my-atom 3) ??(.join thread)) ; wait for thread to finish ?? (println @my-atom) ; -> 4 |
為什么輸出變成4了呢?因為swap!會不停的去給curr-val加一直到成功為止。
Agents
Agents 是用把一些事情放到另外一個線程來做 -- 一般來說不需要事務控制的。它們對于修改一個單個對象的值(也就是Agent的值)來說很方便。這個值是通過在另外的一個thread上面運行一個“action”來修改的。一個action是一個函數, 這個函數接受Agent的當前值以及一些其它參數。 Only one action at a time will be run on a given Agent在任意一個時間點一個Agent實例上面只能運行一個action.
agent 函數可以建立一個新的Agent. 比如:
幫助| 1 | (def my-agent (agent <;em>initial-value</em>)) |
send 函數把一個 action 分配給一個 Agent, 并且馬上返回而不做任何等待。 這個action會在另外一個線程(一般是由一個線程池提供的)上面單獨運行。 當這個action運行結束之后,返回值會被設置給這個Agent。send-off 函數也類似只是線程來自另外一個線程吃。
send 使用一個 "固定大小的" 線程吃 (java.util.concurrent.Executors里面的newFixedThreadPool ) , 線程的個數是機器的處理器的個數加2。如果所有的線程都被占用,那么你如果要運行新的action, 那你就要等了。send-off 使用的是 "cached thread pool" (java.util.concurrent.Executors里面的?newCachedThreadPool) , 這個線程池里面的線程的個數是按照需要來分配的。
如果send 或者send-off 函數是在一個事務里面被調用的。 那么這個action直到線程提交的時候才會被發送給另外一個線程去執行。這在某種程度上來說和commute 函數對 Ref 的作用是類似的。
在action里面, 相關聯的那個agent可以通過symbol:*agent*得到。
await 以一個或者多個Agent作為參數, 并且block住當前的線程,直到當前線程分派給這些Agent的action都執行完了。await-for 函數是類似的, 但是它接受一個超時時間作為它的第一個參數, 如果在超時之前事情都做完了, 那么返回一個非nil的值, 否則返回一個非nil的值,而且當前線程也就不再被block了。await 和await-for 函數不能在事務里面調用。
如果一個action執行的時候拋出一個異常了,那么你要dereference這個Agent的話也會拋出異常的。在action里面拋出的所有的異常可以通過agent-errors 函數獲取。 clear-agent-errors 函數可以清除一個指定Agent上面的所有異常。
shutdown-agents 函數等待所有發送給agents的action都執行完畢。然后它停止線程池里面所有的線程。在這之后你就不能發送新的action了。我們一定要調用shutdown-agents 以讓JVM 可以正常退出,因為Agent使用的這些線程不是守護線程, 如果你不顯式關閉的話,JVM是不會退出的。
Watchers
WARNING: 下面這個章節要做一些更新,因為在Clojure1.1里面add-watcher 和remove-watcher 這兩個函數被去掉了。 兩個不大一樣的函數add-watch 和remove-watch 被添加進來了。
Agents 可以用作其它幾種引用類型的監視器。當一個被監視的引用的值發生了改變之后,Clojure會通過給Agent發送一個action的形式通知它。通知的類型可以是send 或者send-off, 這個是在你把Agent注冊為引用類型的監視器的時候指定的。那個action的參數是那個監視器 Agent 以及發生改變的引用對象。這個action的返回值則是Agent的新值。
就像我們前面已經說過的那樣,函數式編程強調那種“純函數” -- 不會改變什么全局變量的函數。但是Clojure也不絕對靜止這樣做, 但是Clojure使得我們要找出對全局狀態進行了改變的函數非常的簡單。一個方法就是尋找那些能對狀態進行改變的宏和方法,比如alter。 這到了調用這些宏/函數的地方就找到了所有修改全局狀態的地方了。另外一個方法就是用Agent來監視對于全局狀態的更改。一個監視者可以通過dump出來stack trace來確定到底是誰對全局狀態做了修改。
下面的例子給一個Var,一個Ref, 一個Atom注冊了一個Agent監視者。Agent里面維護了它所監視的每個引用被修改的次數(一個map)。這個map的key就是引用對象,而值則是被修改的次數。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | (def my-watcher (agent {})) ?? (defn my-watcher-action [current-value reference] ??(let [change-count-map current-value ????????old-count (change-count-map reference) ????????new-count (if old-count (inc old-count) 1)] ??; Return an updated map of change counts ??; that will become the new value of the Agent. ??(assoc change-count-map reference new-count))) ?? (def my-var "v1") (def my-ref (ref "r1")) (def my-atom (atom "a1")) ?? (add-watcher (var my-var) :send-off my-watcher my-watcher-action) (add-watcher my-ref :send-off my-watcher my-watcher-action) (add-watcher my-atom :send-off my-watcher my-watcher-action) ?? ; Change the root binding of the Var in two ways. (def my-var "v2") (alter-var-root (var my-var) (fn [curr-val] "v3")) ?? ; Change the Ref in two ways. (dosync ??; The next line only changes the in-transaction value ??; so the watcher isn't notified. ??(ref-set my-ref "r2") ??; When the transaction commits, the watcher is ??; notified of one change this Ref ... the last one. ??(ref-set my-ref "r3")) (dosync ??(alter my-ref (fn [_] "r4"))) ; And now one more. ?? ; Change the Atom in two ways. (reset! my-atom "a2") (compare-and-set! my-atom @my-atom "a3") ?? ; Wait for all the actions sent to the watcher Agent to complete. (await my-watcher) ?? ; Output the number of changes to ; each reference object that was watched. (let [change-count-map @my-watcher] ??(println "my-var changes =" (change-count-map (var my-var))) ; -> 2 ??(println "my-ref changes =" (change-count-map my-ref)) ; -> 2 ??(println "my-atom changes =" (change-count-map my-atom))) ; -> 2 ?? (shutdown-agents) |
回到上面
編譯
當clojure的源代碼文件被當作腳本文件執行的時候,它們是在運行時被編譯成java的bytecode的。同時我們也可以提前編譯(AOT ahead-of-time)它們成java bytecode。這會縮短clojure程序的啟動時間,并且產生的.class文件還可以給java程序使用。我們推薦按照下面的步驟來做:
這些步驟會為每個函數創建一個單獨的.class文件。他們會被寫到 "classes" 文件夾下對應的子文件夾下面去。
如果這個被編譯的名字空間有一個叫做-main的函數, 那么你可以把它當作java的主類的運行。命令行參數會被當作參數傳遞給這個函數。比如,如果talk.clj 包含一個叫-main 的函數, 你可以用下面的命令來運行:
幫助| 1 | java -classpath <;em>path</em>/classes:<em>path</em>/clojure.jar com.ociweb.talk <em>args</em> |
在Java里面調用 Clojure
提前編譯的Clojure函數如果是靜態的函數的話,那么它們可以被java程序調用。可以通過把函數的元數據項::static 設置為true 來達到這個目的。語法是這樣的:
幫助| 1 2 3 | (ns <;em>namespace</em> ??(:gen-class ???:methods [#^{:static true} [<em>function-name</em> [<em>param-types</em>] <em>return-type</em>]])) |
讓我們看一個例子:下面是一個名字叫做Demo.clj的文件,它的路徑是src/com/ociweb/clj。
幫助| 1 2 3 4 5 6 7 | (ns com.ociweb.clj.Demo ??(:gen-class ???:methods [#^{:static true} [getMessage [String] String]])) ?? # Note the hyphen at the beginning of the function name! (defn -getMessage [name] ??(str "Hello, " name "!")) |
下面是一個叫做Main.java 的java文件,它和src 以及classes 在同一個目錄。
幫助| 1 2 3 4 5 6 7 8 9 | import com.ociweb.clj.Demo; // class created by compiling Clojure source file ?? public class Main { ?? ????public static void main(String[] args) { ????????String message = Demo.getMessage("Mark"); ????????System.out.println(message); ????} } |
下面是編譯并且運行它的步驟:
Clojure還有一些更加高級的編譯特性。更多細節可以參考宏gen-class 的文檔以及http://clojure.org/compilation/。
回到上面
自動化測試
Clojure里面主要的主要自動化測試框架是clojure core里面自帶的。下面的代碼演示了它的一些主要特性:
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | (use 'clojure.test) ?? ; Tests can be written in separate functions. (deftest add-test ??; The "is" macro takes a predicate, arguments to it, ??; and an optional message. ??(is (= 4 (+ 2 2))) ??(is (= 2 (+ 2 0)) "adding zero doesn't change value")) ?? (deftest reverse-test ??(is (= [3 2 1] (reverse [1 2 3])))) ?? ; Tests can verify that a specific exception is thrown. (deftest division-test ??(is (thrown? ArithmeticException (/ 3.0 0)))) ?? ; The with-test macro can be used to add tests ; to the functions they test as metadata. (with-test ??(defn my-add [n1 n2] (+ n1 n2)) ??(is (= 4 (my-add 2 2))) ??(is (= 2 (my-add 2 0)) "adding zero doesn't change value")) ?? ; The "are" macro takes a predicate template and ; multiple sets of arguments to it, but no message. ; Each set of arguments are substituted one at a time ; into the predicate template and evaluated. (deftest multiplication ??(are [n1 n2 result] ????(= (* n1 n2) result) ; a template ????1 1 1, ????1 2 2, ????2 3 6)) ?? ; Run all the tests in the current namespace. ; This includes tests that were added as function metadata using with-test. ; Other namespaces can be specified as quoted arguments. (run-tests) |
為了限制運行一個test的時候拋出來的異常的深度,bind一個數字到special symbol: *stack-trace-depth* 。
當你要把Clojure代碼編譯成bytecode以部署到生成環境的時候, 你可以給*load-tests* symbol bind一個false 值,以避免把測試代碼編譯進去。
雖然和自動化測試不是同一個層面的東西,還是值得一提的是Clojure提供一個宏:assert 。它測試一個表達式, 如果這個表達式的值為false的話,他們它會拋出異常。這可以警告我們這種情況從來都不應該發生。看例子:
幫助| 1 | (assert (>;= dow 7000)) |
自動化測試的另外一個重要的特性是fixtures。fixture其實就是JUnit里面的setup和tearDown方法。fixture分為兩種,一種是在每個測試方法的開始,結束的時候 執行。一種是在整個測試(好幾個測試方法)的開始和結束的時候執行。
照下面的樣子編寫fixture:
幫助| 1 2 3 4 5 | (defn fixture-name [test-function] ??; Perform setup here. ??(test-function) ??; Perform teardown here. ) |
這個fixture函數會在執行每個測試方法的時候執行一次。這里這個test-function 及時要被執行的測試方法。
用下面的方法去注冊這些fixtures去包裹每一個測試方法:
幫助| 1 | (use-fixtures :each fixture-1 fixture-2 ...) |
執行的順序是:
用下面的方法去注冊這些fixtures去包裹整個一次測試:
幫助| 1 | (use-fixtures <IMG class=wp-smiley alt=:o src="http://xumingming.sinaapp.com/wp-includes/images/smilies/icon_surprised.gif"> nce fixture-1 fixture-2 ...) |
執行順序是這樣的:
Clojure本身的測試在test 子目錄里面. 要想運行他們的話, cd到包含src 和test 的目錄下面去,然后執行: "ant test"。
回到上面
?
編輯器和IDE
Clojure plugins for many editors and IDEs are available. For emacs there is clojure-mode and swank-clojure, both athttps://github.com/technomancy/swank-clojure. swank-clojure uses the Superior Lisp Interaction Mode for Emacs (Slime) described athttp://common-lisp.net/project/slime/. For Vim there is VimClojurehttp://kotka.de/projects/clojure/vimclojure.html. For NetBeans there is enclojure athttp://enclojure.org/. For IDEA there a "La Clojure" athttp://plugins.intellij.net/plugin/?id=4050. For Eclipse there is Counter Clockwise athttp://dev.clojure.org/display/doc/Getting+Started+with+Eclipse+and+Counterclockwise.
桌面程序
Clojure 可以創建基于Swing的GUI程序。下面是一個簡單的例子, 用戶可以輸入他們的名字,然后點擊“Greet:按鈕,然后它會彈出一個對話框顯示一個歡迎信息。可以關注一下這里我們使用了proxy 宏來創建一個集成某個指定類 (JFrame )并且實現了一些java接口 (這里只有ActionListener 一個接口)的對象。.
幫助
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | (ns com.ociweb.swing ??(:import ????(java.awt BorderLayout) ????(java.awt.event ActionListener) ????(javax.swing JButton JFrame JLabel JOptionPane JPanel JTextField))) ?? (defn message ??"gets the message to display based on the current text in text-field" ??1 ??(str "Hello, " (.getText text-field) "!")) ?? ; Set the initial text in name-field to "World" ; and its visible width to 10. (let [name-field (JTextField. "World" 10) ??????greet-button (JButton. "Greet") ??????panel (JPanel.) ??????frame (proxy [JFrame ActionListener] ????????[] ; superclass constructor arguments ????????(actionPerformed [e] ; nil below is the parent component ??????????(JOptionPane/showMessageDialog nil (message name-field))))] ??(doto panel ????(.add (JLabel. "Name:")) ????(.add name-field)) ??(doto frame ????(.add panel BorderLayout/CENTER) ????(.add greet-button BorderLayout/SOUTH) ????(.pack) ????(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) ????(.setVisible true)) ??; Register frame to listen for greet-button presses. ??(.addActionListener greet-button frame)) |
回到上面
Web應用
有很多Clojure類庫可以幫助我們創建web應用。現在比較流行使用Chris Granger寫的Noir。另外一個簡單的,基于MVC的框架, 使用Christophe Grand寫的?Enlive 來做頁面的template, 是Sean Corfield寫的Framework One 。另一個流行的選擇是James Reeves寫的Compojure,你可以在這里下載:http://github.com/weavejester/compojure/tree/master。所有這些框架都是基于Mark McGranahan寫的Ring (James Reeves同學現在在維護). 我們以Compojure為例子來稍微介紹一下web應用開發。最新的版本可以通過git來獲取:
幫助| 1 | git clone git://github.com/weavejester/compojure.git |
這個命令會在當前目錄創建一個叫做compojure 的目錄. 另外你還需要從http://cloud.github.com/downloads/weavejester/compojure/deps.zip下載所有依賴的JAR包,把deps.zip 下載之后解壓在compojure 目錄里面的deps 子目錄里面:
要獲取compojure.jar, 在compojure里面運行ant 命令。
要獲取 Compojure的更新, 切換到compojure 目錄下面執行下面的命令:
幫助| 1 2 | git pull ant clean deps jar |
所有的deps 目錄里面的jar包都必須包含在classpath里面。一個方法是修改我們的clj 腳本,然后用這個腳本來運行web應用. 把 "-cp $CP" 添加到java 命令后面去 執行clojure.main添加下面這些行到腳本里面去,以把那些jar包包含在 CP里面。
幫助| 1 2 3 4 5 6 7 8 9 | # Set CP to a path list that contains clojure.jar # and possibly some Clojure contrib JAR files. COMPOJURE_DIR=<;em>path-to-compojure-dir</em> COMPOJURE_JAR=$COMPOJURE_DIR/compojure.jar CP=$CP:$COMPOJURE_JAR for file in $COMPOJURE_DIR/deps/*.jar do ??CP=$CP:$file done |
下面是他一個簡單的 Compojure web應用:
幫助
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | (ns com.ociweb.hello ??(:use compojure)) ?? (def host "localhost") (def port 8080) (def in-path "/hello") (def out-path "/hello-out") ?? (defn html-doc ??"generates well-formed HTML for a given title and body content" ??[title & body] ??(html ????(doctype :html4) ????[:html ??????[:head [:title title]] ??????[:body body]])) ?? ; Creates HTML for input form. (def hello-in ??(html-doc "Hello In" ????(form-to [:post out-path] ??????"Name: " ??????(text-field {:size 10} :name "World") ??????[:br] ??????(reset-button "Reset") ??????(submit-button "Greet")))) ?? ; Creates HTML for result message. (defn hello-out [name] ??(html-doc "Hello Out" ????[:h1 "Hello, " name "!"])) ?? (defroutes hello-service ??; The following three lines map HTTP methods ??; and URL patterns to response HTML. ??(GET in-path hello-in) ??(POST out-path (hello-out (params :name))) ??(ANY "*" (page-not-found))) ; displays ./public/404.html by default ?? (println (str "browse http://" host ":" port in-path)) ; -> browse http://localhost:8080/hello (run-server {:port port} "/*" (servl |
回到上面
數據庫
Clojure Contrib 里面的jdbc庫簡化了clojure對于關系型數據庫的訪問. 它通過commit和rollback來支持事務, 支持prepared statements, 支持創建/刪除表, 插入/更新/刪除行, 以及查詢。下面的例子鏈接到一個Postgres 數據庫并且執行了一個查詢。代碼的注釋里面還提到了怎么使用jdbc來連接mysql。
幫助| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | (use 'clojure.java.jdbc) ?? (let [db-host "localhost" ??????db-port 5432 ; 3306 ??????db-name "HR"] ?? ??; The classname below must be in the classpath. ??(def db {:classname "org.postgresql.Driver" ; com.mysql.jdbc.Driver ???????????:subprotocol "postgresql" ; "mysql" ???????????:subname (str "//" db-host ":" db-port "/" db-name) ???????????; Any additional map entries are passed to the driver ???????????; as driver-specific properties. ???????????:user "mvolkmann" ???????????:password "cljfan"}) ?? ??(with-connection db ; closes connection when finished ????(with-query-results rs ["select * from Employee"] ; closes result set when finished ??????; rs will be a non-lazy sequence of maps, ??????; one for each record in the result set. ??????; The keys in each map are the column names retrieved and ??????; their values are the column values for that result set row. ??????(doseq [row rs] (println (row :lastname)))))) |
clj-record 提供了一個類似 Ruby on Rails的ActiveRecord的數據庫訪問包. 更多關于它的信息看這里:http://github.com/duelinmarkers/clj-record/tree/master.
回到上面
類庫
很多的類庫提供了Clojure Proper所沒有提供的一些功能, 我們在前面的例子里面已經討論過一些,下面列舉一下沒有提到的一些。并且這里有已知的類庫的一個列表http://clojure.org/libraries。
- clojure.tools.cli - 操作命令行參數并且輸出幫助信息
- clojure.data.xml - 以lazy的方式解析XML
- clojure.algo.monads - 有關monads 的一些方法
- clojure.java.shell - 提供一些函數和宏來創建子進程并且控制它們的輸入/輸出
- clojure.stacktrace - 提供函數來簡化stacktrace的輸出 --- 只輸出跟Clojure有關的東西
- clojure.string - 提供操作字符串以及正則表達式的一些方法
- clojure.tools.trace - 提供跟蹤所有對某個方法的調用的輸出以及返回值的跟蹤
下面是個簡要的例子要使用 clojure.java.shell 獲取當前的工作目錄。
幫助| 1 2 | (use 'clojure.java.shell) (def directory (sh "pwd")) |
回到上面
結論
這篇文章涵蓋了很多的背景知識。如果你想學更多有關Clojure的東西,Stuart Halloway寫了本很不錯的書:"Programming Clojure"。
這篇文章主要關注的是Clojure 1.0的特性, 并且會被社區成員不時的更新的。如果要了解Clojure 1.1以及更新版本的新特性,可以看看這里:http://www.fogus.me/static/preso/clj1.1+/
這里有一些關鍵的問題,你可以問問你自己來看看你到底要不要學習Clojure:
- 你是想要找一種方式使得并發編程更簡單么?
- 你確定能夠接受一種和面向對象完全不同的編程方式:函數式編程么?
- 能運行在JVM上面, 并且可以調用java的類庫,利用java的可移植性對你寫的程序重要么?
- 和靜態類型比起來你更喜歡動態類型么?
- 你覺得Lisp的簡潔的,一致的語法動人么?
如果對于上面某些問題的回答是肯定的,那么你應該考慮嘗試下Clojure。
回到上面
參考文獻
- 我的Clojure網站-http://www.ociweb.com/mark/clojure/
- 我的STM網站-http://www.ociweb.com/mark/stm/
- Clojure官網 -http://clojure.org/
- Clojure API -http://clojure.org/api/
- clj-doc -http://clj-doc.s3.amazonaws.com/tmp/doc-1116/
- Clojure類圖 -http://github.com/Chouser/clojure-classes/tree/master/graph-w-legend.png
- clojure-contrib文檔 -http://code.google.com/p/clojure-contrib/wiki/OverviewOfContrib/
- Wikibooks:Clojure Programming -http://en.wikibooks.org/wiki/Clojure_Programming
- Wikibooks:Learning Clojure -http://en.wikibooks.org/wiki/Learning_Clojure
- Wikibooks:Clojure API Examples -http://en.wikibooks.org/wiki/Clojure_Programming/Examples/API_Examples
- Project Euler Clojure code -http://clojure-euler.wikispaces.com/
- Clojure Snake Game -http://www.ociweb.com/mark/programming/ClojureSnake.html
回到上面
?
總結
以上是生活随笔為你收集整理的Clojure入门教程: Clojure – Functional Programming for the JVM中文版的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle 函数大全(字符串函数,数学
- 下一篇: XP中怎样让批处理文件运行后,不关闭do