clojure 新手指南-目錄 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/141776
無(wú)意中在一個(gè)博客上發(fā)現(xiàn)《clojure 新手指南系列》系列。感覺(jué)很不錯(cuò),于是決定翻譯下來(lái)與大家共享。
先列出整個(gè)系列的目錄:
設(shè)置與安裝
使用REPL來(lái)求值
對(duì)復(fù)雜表達(dá)式求值
代碼保護(hù)
判斷&基本類型
全局綁定&匿名函數(shù)
定義函數(shù)
參數(shù)&重載
元數(shù)據(jù)
與java交互
正則表達(dá)式
本地綁定&詞法作用域
序列&向量
Hash-Maps ,Array-Maps & Sorted Maps
可變性
基本迭代&遞歸
Map , Reduce & Filter
總共17篇,爭(zhēng)取每天翻譯一篇,爭(zhēng)取不太監(jiān)
clojure 新手指南(1)設(shè)置&安裝 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/141813
這篇設(shè)置指南的目標(biāo)讀者是那些沒(méi)有或者很少編程經(jīng)驗(yàn)的人。內(nèi)容包括如何訪問(wèn)windows 命令行接口,如何確定java是否被正確安裝,如何在REPL上運(yùn)行clojure。
步驟一:使用命令行接口
所有的操作系統(tǒng)都包含一個(gè)命令行接口。這種純文字接口建立了人機(jī)交流的通訊入口,讓你借此來(lái)管理系統(tǒng)應(yīng)用和服務(wù)。我們將會(huì)使用這個(gè)接口來(lái)設(shè)置、安裝、和運(yùn)行clojure程序。
在windos系統(tǒng)中,命令行接口就是為人所知的“命令行提示符”。(如何進(jìn)入就不翻譯了)
步驟二:檢查java
clojure需要java才能運(yùn)行。換句話說(shuō),clojure會(huì)被編譯成java字節(jié)碼,這些字節(jié)碼最終會(huì)被依次編譯成機(jī)器碼并被操作系統(tǒng)執(zhí)行。
windows系統(tǒng)默認(rèn)情況下并不會(huì)安裝java。你可以用命令行接口檢查檢查。在命令行中,敲下“java -version" 來(lái)確認(rèn)一下是否安裝。
如果正確安裝,命令行會(huì)做出下面成功的回應(yīng):
java -version java version "1.6.0_20" Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065) Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)
在安裝clojure之前,務(wù)必確保已經(jīng)正確安裝java。
步驟三:安裝clojure
首先下載當(dāng)前最新的穩(wěn)定版本clojure,將其解壓到指定的目錄下。
目錄結(jié)構(gòu)如下:

我們唯一需要的只是clojure-[版本].jar文件。
步驟四:通過(guò)REPL運(yùn)行clojure
關(guān)于REPL:REPL 是 read-eval-print loop的縮寫(xiě)。它能讓你一行行的敲入代碼并能看到運(yùn)行結(jié)果。
終于可以載入clojure程序了。既然clojure離不開(kāi)java,所以你需要通過(guò)使用java命令來(lái)指定clojure文件來(lái)運(yùn)行它。
使用java運(yùn)行clojure(注意路徑和文件名的正確性):
java -cp clojure.jar clojure.main Clojure 1.4.0 user=>
so easy !現(xiàn)在你的命令行接口已經(jīng)成功運(yùn)行了一個(gè)clojure REPL。提示符現(xiàn)在變 成了”user=>",你既可以敲入clojure表達(dá)式,也可以加載clojure程序了。
可選方案一:clojure 與clojure contrib
在進(jìn)入REPL的時(shí)候有一個(gè)可選方案:同時(shí)加載clojure的增強(qiáng)擴(kuò)展包,你可以點(diǎn)擊這里下載。解壓縮后,將contrib.jar拷貝到clojure主目錄(包含clojure.jar的目錄 ),然后在敲擊下面命令:
java -cp contrib.jar;clojure.jar clojure.main Clojure 1.4.0 user=>
可選方案二:clojure 與 JLine
當(dāng)clojure的REPL接管整個(gè)命令行接口時(shí),有一些非常有用的特性變得不可用了。特別是使用上下鍵去查看歷史命令,或者是使用左右鍵去逐字掃描(step through ,這個(gè)翻譯不知道對(duì)不對(duì))當(dāng)前行。
如果你認(rèn)為這些特性很有用,你會(huì)很高興的發(fā)現(xiàn)這些特性也不難設(shè)置。只是簡(jiǎn)單的下載這個(gè)文件,放到你的clojure主目錄。然后使用下面命令:
java -cp jline-0.9.94.jar;clojure.jar jline.ConsoleRunner clojure.main Clojure 1.4.0 user=>
我在clojure1.4.0 的REPL下試了試上下左右鍵,貌似不用jline就可以呀。難道改進(jìn)了?(求指點(diǎn))可選方案三:clojure 與 contrib 、jline
這個(gè)不用解釋,直接上代碼:
java -cp jline-0.9.94.jar;contrib.jar;clojure.jar jline.ConsoleRunner clojure.main Clojure 1.4.0 user=>
clojure 新手指南(2)使用REPL求值 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142086
Clojure 擁有動(dòng)態(tài)語(yǔ)言的所有好處。這意味著你可以在程序被加載后依然可以改變它,并且不用采取額外的步驟去編譯代碼。你既不用停止也不用重啟正在運(yùn)行的應(yīng)用就可以讓修改生效。這對(duì)于其他語(yǔ)言來(lái)說(shuō)可是一個(gè)非常顯著的優(yōu)勢(shì),特別是如果你正打算將變化動(dòng)態(tài)地(不用停止和重啟服務(wù)器)呈獻(xiàn)給用戶時(shí)。
要想改變一個(gè)已經(jīng)加載的程序,你唯一需要的是使用REPL去加載變化的地方。REPL是一個(gè)獨(dú)立的程序,Clojure利用它提供給你和它的編譯器直接交互的功能,當(dāng)然也能與已經(jīng)加載的程序進(jìn)行交互。REPL代表讀?。≧ead)、求值(Evaluate)、打?。≒rint)和 循環(huán)(Loop)。為了使用REPL,你只需要使用操作系統(tǒng)提供的命令行來(lái)運(yùn)行它。
表達(dá)式求值
打開(kāi)你的REPL,隨機(jī)敲入一些字符。很大幾率上Clojure會(huì)及時(shí)地作出相應(yīng)一個(gè)錯(cuò)誤。它可不是什么值都會(huì)接受。
=>ughjava.lang.Exception: Unable to resolve symbol: ugh in this context..
實(shí)際上你會(huì)發(fā)現(xiàn)Clojure只能對(duì)符合語(yǔ)法規(guī)則的表達(dá)式求值。
有一點(diǎn)需要記住,所有的表達(dá)式都會(huì)返回值。即使這個(gè)表達(dá)式什么也不做,它也會(huì)返回值,哪怕僅僅是一個(gè)'nil'(類似java中的null)。
=>(do) //先不用管do是做什么的,其實(shí)什么也不做nil
這是一個(gè)很好的特性,因?yàn)槿绻鹀lojure要是什么都不返回的話,你不知道它是否已經(jīng)執(zhí)行完畢還是陷入了死循環(huán)。
求字面值
我們之前求值都是針對(duì)“表達(dá)式”(expression)。我們?yōu)槭裁床挥谩?strong>代碼”(code)或者“聲明”(statements)來(lái)代替“表達(dá)式”這個(gè)詞呢?一個(gè)理由是“表達(dá)式”這個(gè)詞本身就包含了“代碼"或者”聲明”這兩個(gè)概念。拎一個(gè)重要的原因是clojure可不止是只能對(duì)代碼求值。clojure可以對(duì)“數(shù)據(jù)”(data)求值,這一點(diǎn)與其他語(yǔ)言不同。對(duì)clojure來(lái)說(shuō),代碼即數(shù)據(jù)。(感覺(jué)這個(gè)例子體現(xiàn)的不是太明顯)
數(shù)據(jù)被求值時(shí)僅僅返回自身
=>21.4221.42=>"a string of characters""a string of characters"
Clojure的數(shù)據(jù)操作可不僅僅是字符串或者數(shù)字,實(shí)際上它支持一套非常豐富的數(shù)據(jù)類型和數(shù)據(jù)結(jié)構(gòu)。但是在我們深入clojure之前,還是得先對(duì)著門(mén)語(yǔ)言多一些了解。
clojure 新手指南(3)復(fù)雜表達(dá)式求值 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142111
為了理解復(fù)雜的表達(dá)式和對(duì)它的操作,一個(gè)首要的前提就是理解”前綴表達(dá)式“。這可能會(huì)花費(fèi)你一點(diǎn)時(shí)間來(lái)習(xí)慣它。不過(guò)我相信你會(huì)很快的愛(ài)上這種規(guī)則的。你想想,如果你要對(duì)多個(gè)值進(jìn)行同一種運(yùn)算,你只用寫(xiě)一個(gè)運(yùn)算符在第一個(gè)值的最前面,而不是寫(xiě)多個(gè)運(yùn)算符在中間。不信就看下面的例子:
普通: 1 + 2 + 3 + 4 + 5 + 6 + 8 + 9前綴: + 1 2 3 4 5 6 7 8 9
拋開(kāi)前綴表示法不說(shuō),一個(gè)復(fù)雜表達(dá)式可以看出是一個(gè)單獨(dú)的操作,或者是一組操作。它們既可以接收參數(shù),也能向外輸出,在整個(gè)表達(dá)式計(jì)算完成的時(shí)候返回計(jì)算結(jié)果。一句話,復(fù)雜的表達(dá)式可以做任何你想讓它做的事情。讓我們看一個(gè)非常簡(jiǎn)單的例子:(+ 1 2)
在對(duì)這個(gè)式子求值前,我們先去看一看clojure在求值期間會(huì)做哪些工作:
首先,clojure會(huì)遇到左括號(hào),表明這是一個(gè)列表(表達(dá)式的形式)的開(kāi)始。這個(gè)列表包含了它要處理的所有東西。clojure永遠(yuǎn)會(huì)把左括號(hào)的第一個(gè)元素當(dāng)做是一個(gè)操作符(這也是采用前綴表達(dá)式的原因),這種方式使得lisp方言語(yǔ)法異常的簡(jiǎn)單。在我們的例子中,操作符指的就是一個(gè)函數(shù)。(函數(shù)其實(shí)可以看出是針對(duì)其參數(shù)需要做哪些操作的說(shuō)明)。當(dāng)clojure遇到一個(gè)右括號(hào),表明這個(gè)列表的結(jié)束。這種解析是遞歸的,因?yàn)榱斜碇械脑匾廊豢赡苁橇斜?。這種表達(dá)方式還有另一個(gè)名字:S表達(dá)式。(lisp 的含義就是 list processor 即列表處理)。
S表達(dá)式看起來(lái)可能很直觀,但對(duì)它的理解是非常非常重要的。這是學(xué)習(xí)一切l(wèi)isp方言的基礎(chǔ)。Clojure在執(zhí)行函數(shù)(例如+)之前,首先會(huì)對(duì)其所有的參數(shù)進(jìn)行順序(從左到右)求值并返回自己的結(jié)果。然后函數(shù)會(huì)針對(duì)這些參數(shù)的返回結(jié)果進(jìn)行相應(yīng)的求值運(yùn)算。返回最終的結(jié)果。
上面例子中,”+"是函數(shù),參數(shù)都是數(shù)字字面值。我們說(shuō)過(guò)字面值求值后返回自己。所以整個(gè)操作就是將“+”運(yùn)用到1和2之上。得到的最終結(jié)果就是3。再考慮一下下面這個(gè)例子
(+ 2 (- 8 3))
上面例子稍微復(fù)雜了一點(diǎn),因?yàn)榈诙€(gè)參數(shù)不是單純的字面值。這個(gè)求值也非常簡(jiǎn)單,clojure在對(duì)第二個(gè)參數(shù)(- 8 3)進(jìn)行求值時(shí)采取的依然是之前的策略,得到結(jié)果5。最終 "+"會(huì)作用在2和5之上,最終結(jié)果返回7。所以我們說(shuō)這種解析方式是遞歸的。S表達(dá)式規(guī)則雖然簡(jiǎn)單,但是真的是變化無(wú)窮啊。
再來(lái)看一個(gè)例子(檢測(cè)對(duì)前綴表達(dá)式的理解):
=>(- 8 3 2 1 -6 34 12 4 2 6 4 -23 12 4) -47
了解完上面,接下來(lái)做點(diǎn)啥呢?運(yùn)行點(diǎn)例子也許是個(gè)好主意,但是還有更好的方法。讓我們看一看內(nèi)置函數(shù)的源碼來(lái)了解一下。查看某個(gè)函數(shù)的源碼很簡(jiǎn)單,還是調(diào)用函數(shù)來(lái)做。我們會(huì)使用一個(gè)名為“source”的函數(shù):(source 【函數(shù)名】)
讓我們查看函數(shù)'-'(減號(hào))的源碼
=>(source -) ;;依然是S表達(dá)式 (defn - "If no ys are supplied, returns the negation of x, else subtracts the ys from x and returns the result." {:inline (fn [& args] `(. clojure.lang.Numbers (minus ~ @args))) :inline-arities #{1 2} :added "1.0"} ([x] (. clojure.lang.Numbers (minus x))) ([x y] (. clojure.lang.Numbers (minus x y))) ([x y & more] (reduce - (- x y) more))) nil
這個(gè)比預(yù)想的要多的多了,一個(gè)減號(hào)居然有這么多代碼量?,F(xiàn)在不用擔(dān)心不理解上面的代碼。因?yàn)檫@里面包含了很多未知的知識(shí)。我們只用知道一點(diǎn)是,我們的”-“可以操作任意個(gè)參數(shù)的能力,而這一切歸功一個(gè)叫做”reduce“的函數(shù)。我們不用去調(diào)用”reduce“的源碼,只用看看相關(guān)描述即可。我們可以使用”doc“函數(shù):
=>(doc reduce) ------------------------- clojure.core/reduce ([f coll] [f val coll]) f should be a function of 2 arguments. If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called. If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called. nil
這里的reduce和python中的reduce函數(shù)含義是一樣。
clojure 新手指南(4)代碼保護(hù) - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142297
有時(shí)候,你可能需要防止一個(gè)表達(dá)式或者部分表達(dá)式被執(zhí)行。這種就需要一種稱為“代碼保護(hù)”的技術(shù)。這項(xiàng)技術(shù)使用起來(lái)非常簡(jiǎn)單,就是在表達(dá)式前面加上一個(gè)單引號(hào)“ ‘ ”。clojure 遇到這種前綴加上單引號(hào)的表達(dá)式就會(huì)直接跳過(guò)求值,直接把其當(dāng)做一種叫做“符號(hào)”的數(shù)據(jù)結(jié)構(gòu)。
=>(+ 4 5 3)12=>'(+ 4 5 3)(+ 4 5 3)=>(str '(+ 4 5 3) " is protected while " (+ 4 5 3) " is evaluated.")"(+ 4 5 3) is protected while 12 is evaluated."
關(guān)于符號(hào):這里的單引號(hào)實(shí)際上是另一種形式,叫做quote。'(1 2 3)和(quoto (1 2 3))只是表示相同事物的不同方法而已。quote(或者單引號(hào))可以在任何地方使用,來(lái)阻止Clojure立即對(duì)一個(gè)表達(dá)式求值。實(shí)際上,它的作用遠(yuǎn)不 止于聲明一個(gè)列表,當(dāng)涉及到元編程的時(shí)候,單引號(hào)十分必須。這個(gè)后面在對(duì)符號(hào)作用進(jìn)行詳細(xì)說(shuō)明。
clojure 新手指南(5):判斷&基本類型 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142314
判斷語(yǔ)句
在Clojure中,所有的語(yǔ)法規(guī)則最終都是S表達(dá)式。我們?nèi)绾沃滥男┦桥袛嗾Z(yǔ)句呢?這個(gè)很簡(jiǎn)單,clojure中(lisp習(xí)慣)有個(gè)規(guī)定:對(duì)于判斷功能的函數(shù),函數(shù)名后面都有一個(gè)“?”號(hào)。所以我們只要看到后面帶問(wèn)號(hào)的函數(shù)名,就知道這一定是一個(gè)判斷語(yǔ)句。很簡(jiǎn)單吧!
例如 "fn?"這個(gè)函數(shù)用于判斷傳入的參數(shù)是否是一個(gè)函數(shù):
=>(fn? reduce)true=>(fn? 42)false
基本類型
數(shù)字(Number)
Clojure支持非常豐富的數(shù)字類型的數(shù)據(jù)。每一種數(shù)字類型都提供了不同的計(jì)算精度,當(dāng)然也占用不同的內(nèi)存空間。當(dāng)我們選擇不同的數(shù)據(jù)類型時(shí),精度、內(nèi)存消耗這些因素對(duì)計(jì)算的性能和準(zhǔn)確度有著至關(guān)重要的影響。所以我們必須對(duì)不同的數(shù)據(jù)類型有著深入的了解。
=>4242;;整形=>(class 42)java.lang.Integer;;判斷是否是數(shù)字=>(number? 42)true;;判斷是否是整形=>(integer? 42)true=>21.4221.42;;查看類型=>(class 21.42)java.lang.Double;;判斷是否是數(shù)字=>(number? 21.42)true;;判斷是否是整形=>(integer? 21.42)false
整形和浮點(diǎn)型在其他語(yǔ)言中都是常見(jiàn)的數(shù)據(jù)類型。但是分?jǐn)?shù)(ratios)這種就非常少見(jiàn)了。在Clojure中,你可以將二分之一寫(xiě)成1/2,當(dāng)然也可以用0.5。
=>1/21/2=>(class 1/2)clojure.lang.Ratio=>(ratio? 1/2)true=>(* 1/2 1.0)0.5
使用ratios類型的好處:在進(jìn)行數(shù)據(jù)計(jì)算時(shí),特別是使用大量的除法時(shí),我們可以使用分?jǐn)?shù)形式。等到得到最終的結(jié)果后,我們?cè)趯?duì)分?jǐn)?shù)求值。這樣最大的減少精度損失。(一旦遇到無(wú)法整除的除法運(yùn)算都很可能減少精度)。
字符(Character)
字符代表一個(gè)字母、一個(gè)數(shù)字、一個(gè)標(biāo)點(diǎn)符號(hào)或者其他符號(hào)。在Clojure中,符號(hào)使用反斜杠“\”作為開(kāi)始。
=>\C \C=>(class \C)java.lang.Character=>(char? \C)true
字符串 (String)
字符組成一起就是字符串。字符串使用雙引號(hào)括起來(lái)(這招很通用)。
=>"some characters in a string""some characters in a string"=>(class "some characters in a string")java.lang.String=>(string? "some characters in a string")true=>(str \C)"C"
符號(hào)(Symbol)
符號(hào)被作為一種標(biāo)示符。為了常用目的經(jīng)常綁定到數(shù)據(jù)或者函數(shù)上。符號(hào)之前說(shuō)過(guò)了,用單引號(hào)" ' "開(kāi)頭,或者使用quote函數(shù)。
=>'stuffstuff=> (quote stuff)stuff=>(class 'stuff)clojure.lang.Symbol=>(symbol? 'stuff)true
符號(hào)也可通過(guò)字符串來(lái)創(chuàng)建:=>(symbol "more stuff")more stuff=>(class (symbol "more stuff"))clojure.lang.Symbol
關(guān)鍵字(Keyword)
關(guān)鍵字是另一種類型的符號(hào),它不適用于數(shù)據(jù)綁定的。關(guān)鍵字主要用于匹配。例如在哈希表中作為key,它比使用字符串作為key檢索起來(lái)要快的多。關(guān)鍵字以冒號(hào)”:“開(kāi)頭。
=>:stuff:stuff=>(class :stuff)clojure.lang.Keyword =>(keyword? :stuff)true
關(guān)鍵字也可以通過(guò)字符串來(lái)創(chuàng)建:=>(keyword "stuff"):stuff=>(class (keyword "stuff"))clojure.lang.Keyword
布爾值(Boolean)
這個(gè)太直觀了。就倆值:true 和 false
=>truetrue=>falsefalse=>(class true)java.lang.Boolean=>(class false)java.lang.Boolean
clojure 新手指南(6):全局綁定&匿名函數(shù) - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142394
綁定變量
在Clojure中,我們可以使用 " def " 給函數(shù)和數(shù)據(jù)賦予一個(gè)名字。例如我們定義一個(gè)叫做“alphabet”的字符串變量
user=> (def alphabet "abcdefghijklmnopqrstuvwxyz") #'user/alphabet user=> alphabet "abcdefghigklmnopqrstuvwxyz"
使用變量的好處一個(gè)是方便使用,另一個(gè)在于一個(gè)有意義的名字大大增強(qiáng)代碼的可讀性。舉個(gè)例子,如果我們想要知道一個(gè)字符串的包含的字符數(shù)量:
user=> (count alphabet) 26
我們就不需要在每次去計(jì)算字符串?dāng)?shù)量的時(shí)候都要寫(xiě)下長(zhǎng)長(zhǎng)的一大串了。只用把與該字符串綁定的變量名傳過(guò)去就行。使用變量綁定,我們不僅能使代碼具有良好的可讀性,有時(shí)候還能增加代碼性能。增加性能?這個(gè)很好理解也很常用。例如我們經(jīng)常把計(jì)算好的值綁定到一個(gè)變量上。這樣我們?cè)谟玫竭@個(gè)值的時(shí)候就不用每次都得重新計(jì)算了。對(duì)于一些費(fèi)時(shí)的操作,這可能省下大量的時(shí)間。下面的例子就說(shuō)明了這一點(diǎn):
user=> (def alphabet-length (count alphabet))user=> alphabet-length 26
如果我們要計(jì)算字符串長(zhǎng)度的兩倍,就可以直接使用綁定的字符串長(zhǎng)度:
user=> (+ alphabet-length alphabet-length) 52
使用def綁定類似其他語(yǔ)言中的賦值操作。綁定匿名函數(shù)
首先我們看下面這個(gè)例子:
user=> (def alphabet "abcdefghijklmnopqrstuvwxyz") ;;將aplhabet綁定到一個(gè)字符串 #'user/alphabetuser=> (def alphabet-length (count apphabet)) ;;將alphabet-length與長(zhǎng)度綁定 user=> alphabet ;;此時(shí)為26 26user=> (def alphabet "abc") ;;重新綁定到新的字符串user=> alphabet ;;依然是26,沒(méi)有變化 26
上面例子中,我們改變alphabet后,alphabet-length是沒(méi)有變化的。當(dāng)然對(duì)于上面例子來(lái)說(shuō),沒(méi)有變化是正常的。但是我們?nèi)绾文茏宎lphabet的變化能體現(xiàn)在alphabet-length上呢?這就需要函數(shù)綁定了。我們可以使用關(guān)鍵字 " fn " 來(lái)定義一個(gè)匿名函數(shù)。
user=> (fn[](count alphabet)) ;;這里定義了一個(gè)匿名函數(shù) user$eval6036$fn__6037 user$eval6036$fn__6037@6e3ebbb0user=> (def alphabet-length (fn[](count alphabet))) ;;這里定義了一個(gè)同樣的匿名函數(shù),不過(guò)綁定到了alphabet-length上 #'user/alphabet-length
上面有一個(gè)注意點(diǎn),函數(shù)中使用的alphabet是我們之前已經(jīng)綁定好的。使用def的綁定屬于全局綁定,就是說(shuō)對(duì)于其他函數(shù)是可見(jiàn)的。在clojure中,函數(shù)總會(huì)有一個(gè)返回值,那就是函數(shù)體的執(zhí)行能到達(dá)的最后一個(gè)表達(dá)式。所以上面定義的匿名函數(shù)最終會(huì)返回alphabet的長(zhǎng)度。我們將匿名函數(shù)綁定到了alphabet-length上,每次我們輸入alphabet的時(shí)候,都會(huì)去執(zhí)行這個(gè)匿名函數(shù)。所以我們只要去改變alphabet的值,因?yàn)檫@種改變時(shí)全局的,alphabet-length都會(huì)重新計(jì)算成新的字符串長(zhǎng)度。看下面例子:user=> alphabet-length ;;不執(zhí)行函數(shù),返回函數(shù)字面量 user$alphabet_length user$alphabet_length@71d408f7user=> (alphabet-length) ;;執(zhí)行函數(shù),返回函數(shù)執(zhí)行結(jié)果 3
我們?cè)谑褂媒壎ê瘮?shù)的時(shí)候要注意,此時(shí)alphabet-length是與函數(shù)綁定的,換句話說(shuō)它代表的是一個(gè)函數(shù)。在clojure中,函數(shù)的調(diào)用必須要在S表達(dá)式中。如果直接使用函數(shù)名,會(huì)返回函數(shù)的字面量,并不會(huì)執(zhí)行函數(shù)。我們?cè)賮?lái)看一下如何綁定需要傳參的匿名函數(shù)。我們定義一個(gè)兩個(gè)數(shù)字相加的匿名函數(shù),并綁定到add上:
user=> (def add (fn [a,b](+ a b))) ;; fn后的[]內(nèi)放置參數(shù)列表,多個(gè)參數(shù)用逗號(hào)分隔user=> (add 1 2) 3
綁定匿名函數(shù)的另一種形式
clojure提供了一種比較簡(jiǎn)潔的方式讓我們來(lái)對(duì)匿名函數(shù)進(jìn)行綁定
;; 將 (fn (count alphabet)) 替換成了#(count alphabet) ,省了幾個(gè)括號(hào)user=> (def alphabet-length #(count alphabet)) #'user/alphabet-length
再看一下需要參數(shù)傳遞的例子(非常簡(jiǎn)潔);;這里的參數(shù)直接使用%,如果只有一個(gè)參數(shù)只用 "%"即可。多個(gè)參數(shù)是 "%[參數(shù)序號(hào)]"user=> (def add #(+ %1 %2))user=> (add 1 2) 3
clojure 新手指南(7):定義函數(shù) - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142589
前幾章中,我們用了一種比較迂回的方式創(chuàng)建函數(shù):把匿名函數(shù)綁定到一個(gè)變量上。實(shí)際上,clojure提供了一個(gè)更好的方式做同一件事情?!癲efn” 這個(gè)函數(shù)就是專門(mén)用于定義函數(shù)的。
在我們使用defn之前,我們?cè)倩仡櫼幌轮拔覀冊(cè)趺词褂胐ef來(lái)創(chuàng)建函數(shù)的,然后我們使用defn來(lái)做同一件事情對(duì)比一下。
;;使用def=>(def alphabet-length (fn [ ](count alphabet)))#'user/alphabet-length=>(alphabet-length)26;;使用defn=>(defn alphabet-length [ ](count alphabet))#'user/alphabet-length=>(alphabet-length)26
上面兩種方式做的都是同一件事情。但是defn能做更多的事情。下面是defn定義函數(shù)的一個(gè)腳手架:
[1] (defn name 函數(shù)名 [2] "description" 函數(shù)描述 (可選) [3] {metadata} 元數(shù)據(jù) (可選) [4] [arguments] 參數(shù)列表 [5] body-of-expressions...) 函數(shù)體
上面我們可以看出,defn在定義函數(shù)時(shí)可以提供更多的信息。下面讓我們用上面這些信息定義一個(gè)函數(shù):
=>(defn select-random "從一個(gè)列表中隨機(jī)返回一個(gè)元素" {:added "1.2"} ;; 元數(shù)據(jù) [options] (nth options (rand-int (count options))))#'user/select-random
(count options) 用于計(jì)算options包含的元素?cái)?shù)量。(nth options x) 用于從options中獲取第x個(gè)元素(從0開(kāi)始,類似java中的list的get方法)
我們之前說(shuō)過(guò)clojure是lisp的一種方言。lisp 是 “List Processor”的縮寫(xiě),就是列表解析的意思,使用列表來(lái)表示所有的東西(S表達(dá)式)。從我們寫(xiě)的代碼也可以看出,整個(gè)代碼結(jié)構(gòu)就是一個(gè)嵌套的列表?,F(xiàn)在讓我們用列表結(jié)構(gòu)來(lái)保存數(shù)據(jù):
=>(list "growl" "lick" "jump")("growl" "lick" "jump")
我們之前定義的函數(shù)select-random需要的參數(shù)正是一個(gè)列表,正好我們就可以用來(lái)測(cè)試:=>(select-random (list "growl" "lick" "jump"))"jump"=>(select-random (list "growl" "lick" "jump"))"growl"
運(yùn)行一切正常,說(shuō)明select-random沒(méi)什么問(wèn)題。我們可以在一個(gè)新的函數(shù)中來(lái)使用它。我們來(lái)創(chuàng)建一個(gè)用于問(wèn)候的函數(shù)greeting。=>(defn greeting "Composes a greeting sentence. Expects both the name of a greeter and the name of whom is to be greeted for arguments. An approach and an action are randomly selected." {:added "1.2"} [greeter whom] ;;str 用于組裝字符串 (str greeter " greeted " whom " with a " (select-random (list "ferocious" "wimpy" "precarious" "subtle")) " " (select-random (list "growl" "lick" "jump")) "!")) #'user/greeting=>(greeting "Jon" "Thaddeus")"Jon greeted Thaddeus with a wimpy growl!"=>(greeting "Jon" "Thaddeus")"Jon greeted Thaddeus with a precarious lick!"
當(dāng)然,上面的問(wèn)候函數(shù)不是很完美。我們可以把問(wèn)候語(yǔ)句單獨(dú)提出來(lái)。
=>(def approaches (list "ferocious" "wimpy" "precarious" "subtle"))'user/approaches=>(def actions (list "growl" "lick" "jump"))#'user/actions
然后在greeting中使用綁定的列表:=>(defn greeting "Composes a greeting sentence. Expects both the name of a greeter and the name of whom is to be greeted for arguments. An approach and an action are randomly selected." {:added "1.2"} [greeter whom] (str greeter " greeted " whom " with a " (select-random approaches) " " (select-random actions) "!"))#'user/greeting
現(xiàn)在可讀性好多了吧,把變化的部分單獨(dú)抽象出來(lái)這個(gè)原則對(duì)于函數(shù)式編程也是通用的哦。這樣我們就可以在不修改函數(shù)的情況下改變問(wèn)候語(yǔ)句了。至于函數(shù)定義中的元數(shù)據(jù)有什么作用,暫時(shí)保密,后面會(huì)單獨(dú)來(lái)講。
clojure 新手指南(8):參數(shù)和重載 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142705
現(xiàn)在我們首先定義一個(gè)支持4個(gè)參數(shù)相加的函數(shù):
(defn add [ v1 v2 v3 v4] (+ v1 v2 (if v3 v3 0) (if v4 v4 0) ))
我們想達(dá)到這樣一種效果。如果我們調(diào)用(add 1 2 3 4),則正常返回10。如果我們調(diào)用(add 1 2 3)能得到結(jié)果6?;蛘哒{(diào)用(add 1 2)能得到3。
我們執(zhí)行后發(fā)現(xiàn):
=> (add 1 2 3 4) 10=> (add 1 2)... ArityException Wrong number of args (2) passed to: user$add ...
當(dāng)參數(shù)數(shù)量和我們定義函數(shù)時(shí)的參數(shù)數(shù)量一致時(shí)能得到正確的值。否則會(huì)拋出參數(shù)數(shù)量不匹配異常??磥?lái)這個(gè)和我們預(yù)想的不一樣啊。
固定參數(shù)
我們之前定義函數(shù)時(shí),使用的都是固定參數(shù)方式。調(diào)用函數(shù)時(shí),只要我們傳入?yún)?shù)的數(shù)量不符合定義時(shí)的參數(shù)數(shù)量,clojure都會(huì)提出抗議(拋出參數(shù)數(shù)量不匹配異常)。像上面我們可以這么調(diào)用(add 1 2 3 nil),這樣會(huì)返回6。 (if v4 v4 0)的含義是,如果v4 不為nil或false,就返回v4,否則返回0。我們傳入的正是nil。所以函數(shù)會(huì)正確返回6。當(dāng)然(add 1 2 nil nil )也會(huì)正確返回3。
難道clojure就這么點(diǎn)能力?當(dāng)然不是,clojure的固定參數(shù)可不止這點(diǎn)能耐。我們?cè)谏掀谐龅亩x函數(shù)時(shí)的腳手架其實(shí)并沒(méi)有列完整。請(qǐng)看下面一個(gè)更加完整的腳手架:
(defn variable-name "description" {metadata} ([argument-pattern #1] ;;第一種參數(shù)形式 body-of-expressions) ([argument-pattern #2] ;;第二種參數(shù)形式 body-of-expressions) more-patterns... )
先讓我們用上面方式重構(gòu)一下之前的add函數(shù),看看怎么來(lái)支持2個(gè)、3個(gè)或4個(gè)參數(shù)的相加:
(defn add ( [v1 v2] ( + v1 v2)) ( [v1 v2 v3] (+ v1 v2 v3)) ( [v1 v2 v3 v4] (+ v1 v2 v3 v4)))
上面我們總共定義了三種不同參數(shù)形式的add函數(shù)。這樣調(diào)用的話(add 1 2)會(huì)匹配第一種形式,正確返回3。(add 1 2 3)會(huì)匹配第二種參數(shù)模式,返回結(jié)果6。這其實(shí)就是lisp中模式匹配的一種應(yīng)用。是不是比java中的重載方式更加靈活呀。這只是固定參數(shù)的一種用法,下面再來(lái)看看clojure的可變參數(shù)用法。
可變參數(shù)
如果我們的參數(shù)模式就兩三種(例如上面只需要支持3種不同個(gè)數(shù)的數(shù)字相加),我們可以采用固定參數(shù)+模式匹配來(lái)實(shí)現(xiàn)函數(shù)。但是如果我們的參數(shù)模式個(gè)數(shù)不固定(例如支持任意個(gè)數(shù)字相加),固定參數(shù)+模式匹配也拯救不了我們了。不過(guò)不用擔(dān)心,clojure還給我們提供了可變參數(shù) 。
我們看一下如何使用可變參數(shù)來(lái)實(shí)現(xiàn)任意個(gè)數(shù)的數(shù)字相加:
(defn add [v1 v2 & others] ;;&后面的是可變參數(shù) (+ v1 v2 (if others ;;判斷可變參數(shù)列表是否是空,如果不是累加列表中的值,否則返回0 (reduce + 0 others) ;;使用reduce函數(shù)計(jì)算others的數(shù)字之和。 0 ) ))
這里有幾點(diǎn)需要注意一下,固定參數(shù)要寫(xiě)在一個(gè)“&”之后,只能有一個(gè)可變參數(shù)。&后的可變參數(shù)名代表的是由可變參數(shù)組成的列表。這里的reduce函數(shù),先理解成對(duì)列表中的數(shù)字進(jìn)行累加就行,本文目的主要是理解可變參數(shù)的用法。
現(xiàn)在我們執(zhí)行一下上面定義的函數(shù):
=> (add 1 2) 3=> (add 1 2 3) 6=> (add 1 2 3 4 5 6) 21
現(xiàn)在add已經(jīng)支持任意個(gè)數(shù)字參數(shù)相加了。
clojure 新手指南(9):元數(shù)據(jù) - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/142982
我們?cè)诙x函數(shù)的時(shí)候提到了如何去定義一個(gè)元數(shù)據(jù)。但之前只是定義它,并沒(méi)有明說(shuō)它的用途。讓我們?cè)倏匆幌轮岸x的select-random函數(shù),我們添加了一個(gè)叫做:add的元數(shù)據(jù)。注意:元數(shù)據(jù)是以哈希表形式展現(xiàn)的。(鍵和值可以是任何類型,不過(guò)key一般推薦為關(guān)鍵字類型)
=>(defn select-random "從一個(gè)列表中隨機(jī)返回一個(gè)元素" {:added "1.2"} ;; 元數(shù)據(jù) [options] (nth options (rand-int (count options))))#'user/select-random
我們可以使用下面方式去查看一個(gè)函數(shù)的元數(shù)據(jù)信息(一個(gè)哈希表):
=>(meta #'select-random){:ns @<Namespace user>@, :name select-random, :file "NO_SOURCE_PATH", :line 1, :arglists ([options]), :added "1.2", :doc "從一個(gè)列表中隨機(jī)返回一個(gè)元素"}
我們雖然只定義了一個(gè)元數(shù)據(jù):add,但是系統(tǒng)卻給我們返回了一堆元數(shù)據(jù)。這些元數(shù)據(jù)是系統(tǒng)默認(rèn)給函數(shù)添加了,主要是函數(shù)的一些基本信息。下面是一些比較重要的信息:
:ns 命名空間
:name 函數(shù)名
:file 對(duì)應(yīng)的源碼文件
:arglists 參數(shù)列表 (一個(gè)函數(shù)刻意包含多個(gè)參數(shù)列表(見(jiàn)上篇),所以是lists 而不是list)
:doc 函數(shù)描述
下面是一些元數(shù)據(jù)的使用場(chǎng)合:
1、定義函數(shù)時(shí),可以添加對(duì)應(yīng)的clojure的版本。這樣一旦clojure升級(jí),你可以系統(tǒng)的測(cè)試任何相關(guān)的函數(shù)。
2、做一些類似java注解方面的工作。例如,如果函數(shù)已不再使用,可以添加:state "deprecated"。
3、給函數(shù)添加一些統(tǒng)計(jì)信息等等。
我們可不僅限于只給函數(shù)添加元數(shù)據(jù)。任何能綁定變量的都可以添加元數(shù)據(jù),例如符號(hào)或者其他數(shù)據(jù)結(jié)構(gòu)。
=>(def approaches (with-meta (list "ferocious" "wimpy" "precarious") {:creator "tim"}))#'user/approaches=>(meta approaches){:creator "tim"}
clojure 新手指南(10):與java交互 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/143000
clojure是基于jvm的語(yǔ)言,就是說(shuō)clojure會(huì)被編譯成字節(jié)碼被jvm執(zhí)行。但是clojure能做的可不僅僅是被編譯成字節(jié)碼,它提供了一套API讓用戶與java交互。因此clojure可以直接調(diào)用java世界中那些豐富龐大的優(yōu)秀庫(kù)了。
數(shù)據(jù)&轉(zhuǎn)換
=>12.5612.56
在clojure中,我們使用數(shù)據(jù)時(shí)似乎并沒(méi)有像其他語(yǔ)言那樣需要一些特殊的處理或者聲明。但是,clojure在底層實(shí)際創(chuàng)建了java對(duì)象,并指定了對(duì)應(yīng)的java類型。我們可以通過(guò)class函數(shù)來(lái)看一下編譯器為我們創(chuàng)建的數(shù)據(jù)的類型。=>(class 12.56)java.lang.Double
上面我們可以看出,clojure自動(dòng)為我們創(chuàng)建了一個(gè)Double類型的java對(duì)象。如果我們想在clojure指定對(duì)應(yīng)的java類型,可以這么做:
=>(new java.lang.Float 12.56)12.56=>(class (new java.lang.Float 12.56))java.lang.Float
new 也是一個(gè)函數(shù),我們使用它創(chuàng)建了一個(gè)Float類型的對(duì)象,貌似比java還麻煩。clojure給我們提供了一個(gè)更簡(jiǎn)潔的語(yǔ)法來(lái)做同樣的事情。我們?cè)趯?duì)應(yīng)的構(gòu)造函數(shù)名字后加一個(gè)點(diǎn),然后后面依次寫(xiě)上構(gòu)造函數(shù)需要的參數(shù)即可。=>(Float. "12.56")12.56=>(class (Float. "12.56"))java.lang.Float
這里 Float. 就是Float的構(gòu)造函數(shù)名加上一個(gè)點(diǎn)。這可不是clojure的函數(shù)調(diào)用。我們可以使用fn?測(cè)試一下,F(xiàn)loag.并不是函數(shù)。應(yīng)該只是clojure的特殊的語(yǔ)法調(diào)用吧。下面是一些將字符串轉(zhuǎn)換為數(shù)字的例子:
=>(+ "12.56" "5.92")java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number=>(class "12.56")java.lang.String=>(+ (Float. "12.56")(Float. "5.92"))18.48=>(+ (Float. "12.5648230948234")(Float. "5.92"))18.484823=>(+ (Double. "12.5648230948234")(Double. "5.92"))18.484823094823398
日期和時(shí)間函數(shù)
clojure本身并沒(méi)有專門(mén)的日期和時(shí)間處理函數(shù)。不過(guò)不用擔(dān)心,java有的功能,clojure基本上都可以去調(diào)用。而且一旦我們用clojure函數(shù)將java的方法包裝后,我們后面的調(diào)用就再也不會(huì)和java打交道了。
下面就看看如何使用java接口來(lái)創(chuàng)造我們自己的日期時(shí)間函數(shù)。讓我們從調(diào)用系統(tǒng)當(dāng)前時(shí)間開(kāi)始吧。這一次我們先采用一種“斜線調(diào)用”形式。我們使用斜線一次隔開(kāi)類名和調(diào)用的靜態(tài)方法名稱。我們將要調(diào)用java中的** **System.currentTimeMillis()方法來(lái)獲取當(dāng)前系統(tǒng)時(shí)間的毫秒數(shù)。
=>(defn msec [] ;;使用函數(shù)msec包裝 (System/currentTimeMillis)) ;;這里的斜線相當(dāng)于java中的 "."#'user/msec=>(msec) ;;調(diào)用msec,返回當(dāng)前系統(tǒng)時(shí)間毫秒數(shù) 1307328017335=>(msec)1307662973116=>(> 1307328017335 1307662973116)false=>(> 1307662973116 1307328017335)true
上面是靜態(tài)方法調(diào)用的展示,下面再來(lái)看看如果調(diào)用實(shí)例方法。讓我們創(chuàng)建兩個(gè)java.util.Date對(duì)象。注意,之前我們使用的都是java.lang下面的類,所以不需要手動(dòng)導(dǎo)入。非lang包下的必須手動(dòng)導(dǎo)入(和java一樣一樣的)。
=>(Date.) ;;導(dǎo)入前,找不到Date類java.lang.IllegalArgumentException: Unable to resolve classname: Date...=>(import java.util.Date) ;;手動(dòng)導(dǎo)入Date (語(yǔ)法都和java一樣)java.util.Date =>(Date.) ;;再次創(chuàng)建Date對(duì)象 #<Date Sun Jun 05 20:48:19 MDT 2011>=>(class (Date.)) ;;查看一下對(duì)象類型,確實(shí)是 java.util.Datejava.util.Date
現(xiàn)在我們來(lái)用clojure來(lái)包裝一下,我們創(chuàng)造一個(gè)名為date的函數(shù)。該函數(shù)有兩種參數(shù)形式:無(wú)參數(shù)和接受毫秒值(對(duì)應(yīng)Date 的無(wú)參構(gòu)造函數(shù)和 Date(long time)構(gòu)造函數(shù))。
=>(defn date ;;使用函數(shù)date封裝java API () ;;第一種是無(wú)參數(shù)模式 ([systime](Date. systime))) ;;第二種是接受一個(gè)參數(shù)#'user/date=>(date) ;;調(diào)用date函數(shù),無(wú)參數(shù)#<Date Sun Jun 05 20:41:42 MDT 2011>=>(date 1307328017335) ;;調(diào)用date函數(shù),傳入一個(gè)long值#<Date Sun Jun 05 20:40:17 MDT 2011>
為了讓我們時(shí)間處理在強(qiáng)大點(diǎn),我們?cè)诶胘ava庫(kù)中的 SimpleDateFormat類將時(shí)間轉(zhuǎn)換成特定形式的字符串。
=>(import java.text.SimpleDateFormat) ;;導(dǎo)入需要的類java.text.SimpleDateFormat=>(defn format-date ;;使用自定義函數(shù)封裝 ([](format-date (date) "yyyy MM dd HH mm ss")) ([x](if (string? x) (format-date (date) x) (format-date x "yyyy MM dd HH mm ss"))) ([dt fmt](.format (SimpleDateFormat. fmt) dt)))#'user/format-date=>(format-date)"2011 06 04 17 50 21"=>(format-date (date 404534000000))"1982 10 26 20 33 20"=>(format-date "yyyy/MM/dd HH:mm:ss")"2011/06/04 17:51:00"=>(format-date (date 404534000000) "yyyy/MM/dd HH:mm:ss")"1982/10/26 20:33:20"
其他沒(méi)有什么難點(diǎn),都是之前說(shuō)過(guò)的內(nèi)容,我們主要解釋上面代碼中下面這一句的意義:
([dt fmt](.format (SimpleDateFormat. fmt) dt)))
[dt fmt]是參數(shù)列表,這個(gè)沒(méi)什么特別的東西。主要看看函數(shù)體。 .format 是方法名 (SimpleDateFormat. fmt)最終返回的是 SimpleDateFormat類型的對(duì)象(相當(dāng)于調(diào)用 new SimpleDateFormat(fmt)) ,dt 是參數(shù)所以整個(gè)意思是調(diào)用實(shí)例 (SimpleDateFormat. fmt)的format 方法,并傳入?yún)?shù)dt。翻譯成java代碼就是:(new SimpleDateFormat(fmt)).format(dt)
很簡(jiǎn)單吧。個(gè)人覺(jué)得使用clojure封裝后的java代碼比原本的java API更要緊湊和靈活。這主要依靠了clojure這種動(dòng)態(tài)語(yǔ)言+函數(shù)式語(yǔ)言的N多優(yōu)勢(shì)。
clojure 新手指南(11):正則表達(dá)式 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/143294
接著上篇,繼續(xù)我們的時(shí)間和日期函數(shù)的探討。我們可以定義一個(gè)函數(shù),將一個(gè)日期字符串分成一個(gè)列表。列表元素分別為年、月、日、時(shí)、分、秒。為了完成這個(gè)字符串分割操作,我們使用“re-split‘函數(shù)。re-split函數(shù)需要一個(gè)正則表達(dá)式參數(shù)用于確定如何分割字符串。Clojure 依賴java的正則表達(dá)式庫(kù)來(lái)處理這些操作。
re-split函數(shù)是Clojure Contrib中字符串庫(kù)的一部分,所以這就需要確保你能訪問(wèn)Clojure Contrib庫(kù)。你可以用下面這種方式來(lái)加載字符串工具庫(kù)。(注意,記得進(jìn)入REPL時(shí)要加載contrib.jar,忘了點(diǎn)這 )
=> (use 'clojure.contrib.str-utils)nil
一旦加載完庫(kù),我們就可以使用re-split了:
=>(re-split #" " "2011 06 04 17 50 21")("2011" "06" "04" "17" "50" "21")=>(class #" ")java.util.regex.Pattern=>(re-split #":" "2011:06:04:17:50:21")("2011" "06" "04" "17" "50" "21")
我們上面用到的正則表達(dá)式非常直白。它們以”#“開(kāi)頭,后面跟著包含需要匹配的正則模式。正則表達(dá)式非常復(fù)雜,不是本文重點(diǎn),這里只是講解clojure的相關(guān)用法。
讓我們隨便看幾個(gè)例子:
=>(re-split #":" "2011:06:04:17:50:21")("2011" "06" "04" "17" "50" "21")
方括號(hào)里代表符合其中一個(gè)即可:
=>(re-split #"[/:]" "2011/06/04 17:51:00")("2011" "06" "04 17" "51" "00")
最后我們嘗試構(gòu)建自己的日期元素列表函數(shù)date-list
=>(defn date-list ([](re-split #"\W+" (format-date))) ([systime](re-split #"\W+" (format-date systime))))=>(date-list)("2011" "06" "05" "11" "21" "21")
本章沒(méi)什么新內(nèi)容,主要難點(diǎn)就在正則表達(dá)式上。不過(guò)這個(gè)和clojure沒(méi)什么關(guān)系了。
clojure 新手指南(12):本地綁定&詞法作用域 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/143803
如果你還沒(méi)忘的話,仔細(xì)回想一下,之前我們是如何將對(duì)象綁定到變量名上的。但當(dāng)時(shí)我們只是全局綁定,在那時(shí)這種綁定是非常有用的。不過(guò),有很多時(shí)候,本地綁定往往比全局綁定更合適,例如把變量限制在一個(gè)操作內(nèi)部的時(shí)候。下面就讓我們看看如果使用綁定函數(shù) "let " 進(jìn)行本地綁定。
=>idjava.lang.Exception: Unable to resolve symbol: id...=>(let [id 1] (println id))1nil=>idjava.lang.Exception: Unable to resolve symbol: id...
正向你看到的南陽(yáng),通過(guò)使用"let" 操作,我們把1綁定到了”id“這個(gè)變量名上。然后我們又把它打印了出來(lái)。當(dāng)這個(gè)操作執(zhí)行完后,我們?cè)谕饷娌榭础眎d“時(shí)卻是無(wú)法解析的。這就證明了變量”id“只存在于操作內(nèi)部(本地綁定類似于java中的方法局部變量)。
本地綁定可以將變量限制在某個(gè)操縱內(nèi),這樣就不會(huì)造成對(duì)其他操作的變量污染。試想一下,如果沒(méi)有本地綁定,一旦我們使用了id這個(gè)變量名后,我們就再也不能使用它來(lái)綁定其他對(duì)象了。使用本地綁定后,我們可以在某個(gè)操作內(nèi)使用任何有意義的變量名而不用擔(dān)心和其他相同名字的變量造成沖突,即使是全局變量:
=>(def id 0) ;;全局綁定#'user/id=>id0=>(let [id 1] ;;本地綁定 (println id))1nil=>id ;;本地綁定對(duì)全局綁定沒(méi)有任何影響0
這種行為通常被稱為詞法作用域,我們可以保護(hù)變量不受污染,甚至是父操作也存在同樣的變量名:
=>(let [id 1] ;;外層操作 (let [id 2] ;;內(nèi)層操作 (println id)) ;;內(nèi)層本地綁定的id (println id)) ;;外層本地綁定的id 21nil
我們?cè)賮?lái)舉一個(gè)例子。上一篇最后我們寫(xiě)了一個(gè)”data-list"函數(shù),這個(gè)函數(shù)最終返回給我們一個(gè)包含各個(gè)時(shí)間元素的列表。我們每調(diào)用一次,它都會(huì)返回給我們當(dāng)前時(shí)間年、月、日、時(shí)、分、秒組成的一個(gè)列表:
=>(date-list)("2013" "07" "10" "15" "02" "59")=>(date-list)("2013" "07" "10" "15" "03" "02")
現(xiàn)在呢,我們想要這么一個(gè)函數(shù) run-report,通過(guò)這個(gè)函數(shù),我們能只打印出"時(shí)"和"分"這兩個(gè)元素。這個(gè)簡(jiǎn)單我們可以向下面這樣去實(shí)現(xiàn)它:
=>(defn run-report [] (str "report ran: " (nth (date-list) 3) ":" (nth (date-list) 4)))#'user/run-report=>(run-report)"report ran: 15:04"
上面這個(gè)函數(shù)有什么問(wèn)題嗎?聰明的你就會(huì)發(fā)現(xiàn)data-list這個(gè)函數(shù)被調(diào)用了兩次。一次我們用來(lái)獲取小時(shí),一次我們用來(lái)獲取分鐘。這樣做的話有兩個(gè)壞處。第一個(gè)是,這兩次調(diào)用返回的時(shí)間是不一樣的(函數(shù)在快也需要時(shí)間執(zhí)行),我們很可能得到非常錯(cuò)誤的結(jié)果。假如第一次調(diào)用恰好是15:59:59,到了接近16點(diǎn)的臨界點(diǎn)。第二次調(diào)用變成了16:00:00。這兩個(gè)組合就變成了 15:00 。第二個(gè)壞處就是,排除第一個(gè)錯(cuò)誤的話,如果data-list執(zhí)行時(shí)間比較長(zhǎng),多次調(diào)用勢(shì)必影響函數(shù)效率。
更好的做法就是我們只調(diào)用一次data-list,然后把調(diào)用后的結(jié)果綁定到一個(gè)本地變量上:
=>(defn run-report [ ] (let [date (date-list)] ;;data-list是全局變量。date是本地變量 (str "report ran " (nth date 3) ":" (nth date 4))))#'user/run-report=>(run-report)"report ran: 15:09"
通過(guò)上面的做法,之前的兩個(gè)問(wèn)題都不復(fù)存在了。
下面是另一種做法:
=>(defn run-report [date] (str "report ran: " (nth date 3) ":" (nth date 4)))#'user/run-report=>(run-report (date-list)) ;; 傳入?yún)?shù)就是一種隱式的本地綁定"report ran: 15:10"
我們給run-raport 函數(shù)添加了一個(gè)參數(shù)date,參數(shù)對(duì)函數(shù)來(lái)說(shuō)就是一個(gè)隱式的本地綁定。當(dāng)函數(shù)被執(zhí)行時(shí),用實(shí)參替換形參的時(shí)候,本地綁定就自動(dòng)的和隱式的進(jìn)行了。
關(guān)于詞法作用域可以參考這篇文章,雖然是關(guān)于javascript的,但道理是一樣的。
clojure 新手指南(13):序列&向量 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/144421
序列
我們知道clojure是Lisp的一種方言,那么這也意味著對(duì)這門(mén)語(yǔ)言必然植根于“列表解析”。但是在Clojure中,我們優(yōu)先使用"序列"來(lái)創(chuàng)造列表和管理列表中的元素。
列表
之前我們說(shuō)過(guò),Lisp系列語(yǔ)言整個(gè)都建立在列表之上。我們使用"list"函數(shù)來(lái)創(chuàng)建一個(gè)列表,但后面你就會(huì)發(fā)現(xiàn)創(chuàng)建列表的方式不只一種。如果你不想讓你列表中的元素被解釋執(zhí)行,記得引用(quote)一下。
=> (list "truck" "car" "bicycle" "plane") ;;創(chuàng)建一個(gè)列表 ("truck" "car" "bicycle" "plane");;與上面方式等價(jià)=> '("truck" "car" "bicycle" "plane") ("truck" "car" "bicycle" "plane");;查看列表的類型=>(class '("truck" "car" "bicycle" "plane"))clojure.lang.PersistentList;; 給創(chuàng)建的列表綁定一個(gè)全局變量 => (def vehicles (list "truck" "car" "bicycle" "plane"))#'user/vehicles;;查看是否是序列=>(seq? vehicles)true;;查看是否是列表 =>(list? vehicles)true;;查看是否是集合 => (coll? vehicles)true;;獲取第一個(gè)元素=>(first vehicles)"truck";;獲取第二個(gè)元素 =>(second vehicles)"car";;獲取最后一個(gè)元素=>(last vehicles)"plane";;獲取除第一個(gè)元素以外的剩下列表=>(rest vehicles)("car" "bicycle" "plane");;獲取第n個(gè)元素=>(nth vehicles 0)"truck"=>(nth vehicles 1)"car";;添加元素 (這只是返回一個(gè)新的列表,vehicles 并不會(huì)被改變)=> (conj vehicles "motorcycles")("motorcycles" "truck" "car" "bicycle" "plane")
Cons
Cons是lisp語(yǔ)言中另一個(gè)類似列表的一種數(shù)據(jù)結(jié)構(gòu)。術(shù)語(yǔ)”cons“意思就是構(gòu)造一個(gè)對(duì)(pair),將這些對(duì)鏈接在一起然后形成一個(gè)類似列表的數(shù)據(jù)結(jié)構(gòu)。就像list一樣,cons既是一個(gè)類型,也是一個(gè)函數(shù),我們可以使用cons來(lái)創(chuàng)建這種數(shù)據(jù)結(jié)構(gòu)。
=>(cons "truck" (list "car" "bicycle" "plane"))("truck" "car" "bicycle" "plane")=>(class (cons "truck" (list "car" "bicycle" "plane")))clojure.lang.Cons=>(def vehicles (cons "truck" (list "car" "bicycle" "plane")))#'user/vehicles=>(seq? vehicles)true=>(list? vehicles)false=>(coll? vehicles)true=>(conj vehicles "motorcycle")("motorcycle" "truck" "car" "bicycle" "plane")=>(class (conj vehicles "motorcycle"))clojure.lang.Cons=>(cons "motorcycle" vehicles)("motorcycle" "truck" "car" "bicycle" "plane")=>(class (cons vehicles "motorcycle"))clojure.lang.Cons=>(conj "truck" nil)java.lang.ClassCastException: cannot be cast to clojure.lang.IPersistentCollection=>(cons "truck" nil)("truck")=>(class (cons "truck" nil))clojure.lang.PersistentList
注意最后一個(gè)例子,是不是看起來(lái)很奇怪?當(dāng)我們使用cons將一個(gè)元素附加到一個(gè)列表或者另一個(gè)cons結(jié)構(gòu)上時(shí),返回的仍然是一個(gè)cons類型結(jié)構(gòu)。但是當(dāng)我們將一個(gè)item附加nil上時(shí),返回的卻是list類型。這一點(diǎn)尤其注意。
向量
向量是除了list和cons之外的另一個(gè)很受歡迎的數(shù)據(jù)結(jié)構(gòu),因?yàn)樗袝r(shí)用起來(lái)有一些獨(dú)特的優(yōu)勢(shì)。舉個(gè)例子,因?yàn)橄蛄渴褂梅嚼ㄌ?hào)來(lái)表示,所以至少?gòu)囊曈X(jué)上來(lái)說(shuō)會(huì)讓它從大量的圓括號(hào)中脫穎而出,提供了更好的可讀性。另外使用向量通常能提供比列表更好的性能優(yōu)勢(shì)。
=>(vector "truck" "car" "bicycle" "plane")["truck" "car" "bicycle" "plane"]
;;一種簡(jiǎn)便的創(chuàng)建向量方式,這個(gè)不需要“引用”了哦=>["truck" "car" "bicycle" "plane"]["truck" "car" "bicycle" "plane"]=>(class ["truck" "car" "bicycle" "plane"])clojure.lang.PersistentVector=>(def vehicles ["truck" "car" "bicycle" "plane"])#'user/vehicles=>(seq? vehicles)false=>(list? vehicles)false=>(vector? vehicles)true=>(coll? vehicles)true
注意:雖然大多數(shù)函數(shù)對(duì)待向量和列表都會(huì)產(chǎn)生相同的結(jié)果,但有時(shí)候這種假設(shè)往往會(huì)引入一些問(wèn)題??聪旅胬樱⒁舛咧g的區(qū)別:=> (conj ["a" "b"] "c")["a" "b" "c"]=> (conj '("a" "b") "c")("c" "a" "b")
clojure 新手指南(14):Hash-Maps ,Array-Maps & Sorted... - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/145319
hash-map
創(chuàng)建
在clojure中,哈希表是最通用的一種Map,和java中的HashMap一樣,它們?cè)谔幚泶罅繑?shù)據(jù)方面效率非常高,但是不保證順序。我們可以使用函數(shù)hash-map來(lái)創(chuàng)建哈希表:
=>(hash-map :truck "Toyota" :car "Subaru" :plane "de Havilland"){:plane "de Havilland", :truck "Toyota", :car "Subaru"}=>(class (hash-map :truck "Toyota" :car "Subaru" :plane "de Havilland"))clojure.lang.PersistentHashMap
在clojure中,所有的類型對(duì)象都可以作為map的鍵,甚至是函數(shù)對(duì)象也可以。但是我們推薦使用關(guān)鍵字類型(以冒號(hào)開(kāi)頭)作為map的鍵,因?yàn)殛P(guān)鍵字作為鍵時(shí)哈希表性能最好。創(chuàng)建哈希表時(shí)不一定非得用hash-map 函數(shù),下面就是一個(gè)更簡(jiǎn)便的方式:
;;直接使用{}來(lái)創(chuàng)建哈希表=>{:truck "Toyota" :car "Subaru" :plane "de Havilland"}{:truck "Toyota", :car "Subaru", :plane "de Havilland"}
不過(guò),用上面這種方式,如果傳入的鍵值對(duì)少于9個(gè),它實(shí)際上創(chuàng)建的是ArrayMap而不是HashMap:
=>(class {:truck "Toyota" :car "Subaru" :plane "de Havilland"})clojure.lang.PersistentArrayMap
一旦你把它綁定到一個(gè)變量上,它自動(dòng)就轉(zhuǎn)換成了hashMap:
=> (def map {test: 1})#'user/map=> (class map)clojure.lang.PersistentHashMap
ArrayMap 和 HashMap這種轉(zhuǎn)換,看起來(lái)很詭異。但是不要擔(dān)心,因?yàn)榇蟛糠智闆r下,這都不會(huì)引起問(wèn)題。我見(jiàn)過(guò)的所有函數(shù)對(duì)這兩種map的操作都是一樣的,沒(méi)有任何區(qū)別。
創(chuàng)建map時(shí),如果你想讓鍵值對(duì)之間的分隔更清晰,可以使用逗號(hào)分隔符:
;;使用hash-map函數(shù)=> (hash-map :key1 1 , :key2 2){:key2 2, :key1 1};;直接創(chuàng)建=>{:key1 1 , :key2 2}{:key2 2, :key1 1}
我們?cè)賮?lái)看看和map相關(guān)的操作
讀取
;;如果是map的鍵是關(guān)鍵字類型,關(guān)鍵字直接可以當(dāng)做函數(shù)使用=> (def m {:key1 1, :key2 2}){:key1 1, :key2 2};;獲取:key1對(duì)應(yīng)的值=> (:key1 m)1;;map本身也可以用于取值,這種適應(yīng)于任意類型的key=> (m :key1)1
一種更好的方式是使用get函數(shù),因?yàn)榭梢栽O(shè)置缺省值。(get 也可以用于向量,把key變成索引值即可)
=> (def m {:key1 1 :key2 :2});;獲取 :key1對(duì)應(yīng)的值=> (get m :key1)1;;如果 :key3不存在,返回缺省值=> (get m :key3 "default")"default"
我們可以使用get-in 獲取嵌套map的值
;;創(chuàng)建嵌套mapuser=> (def m {:username "sally" :profile {:name "Sally Clojurian" :address {:city "Austin" :state "TX"}}})
'user/muser=> (get-in m [:profile :name])"Sally Clojurian"user=> (get-in m [:profile :address :city])"Austin"user=> (get-in m [:profile :address :zip-code])nil;;如果鍵不存在,可以設(shè)置默認(rèn)值 user=> (get-in m [:profile :address :zip-code] "no zip code!")"no zip code!"
我敢說(shuō)這是json好么?。?!(get-in 函數(shù)可不只是用于map,向量也可以使用get-in操作)
增加/修改
;;沒(méi)則增加,有則修改 =>(conj {:name "qh" :age 20} {:age 30} {:gender 'male}){:gender mail, :age 30, :name "qh"};;沒(méi)則增加,有則修改=>(merge {:name "qh" :age 20} {:age 30} {:gender 'male}){:gender mail, :age 30, :name "qh"};;assoc是操作map和對(duì)應(yīng)的元素,上面兩個(gè)操作的是多個(gè)map,注意區(qū)別=>(assoc {:name "qh" :age 20} :age 30 :gender 'male){:gender mail, :age 30, :name "qh"}
刪除
;;刪除:name對(duì)應(yīng)的鍵值對(duì)=>(dissoc {:name "qh" :age 30} :name){:age 30};;給m綁定一個(gè)map=> (def m {:name "qh" :age 30})#'user/m;;執(zhí)行刪除操作=>(dissoc m :name){:age 30};;m沒(méi)有變化=>m{:name "qh" :age 30}
我們的刪除操作只是返回一個(gè)新的map,并不會(huì)對(duì)原有的map造成影響。這點(diǎn)要注意。這也是函數(shù)式編程中強(qiáng)調(diào)的"消除副作用"。之前的添加和修改都是如此。
獲取所有的key
;;使用keys函數(shù)=> (keys {:name "clo" :age 30})(:name :age)
獲取所有的value
;;使用vals函數(shù)獲取所有value=> (vals {:name "clo" , :age 30})("clo" 30)
Sorted Maps
如果我們想要?jiǎng)?chuàng)建一個(gè)有序的map(按照key的自然順序),可以使用sorted-map函數(shù)來(lái)創(chuàng)建一個(gè)有序map。
user=> (def sm (sorted-map :c 1 :b 2 :f 3 :a 3))#'user/smuser=> sm{:a 3, :b 2, :c 1, :f 3}
有序map增刪改查操作和上面一樣。
clojure 新手指南(15):可變性 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/148993
我們已經(jīng)知道如何把數(shù)據(jù)綁定到一個(gè)變量上,這給我們提供了一種可共享的數(shù)據(jù)的持久化存儲(chǔ)方式(數(shù)據(jù)被綁定到一個(gè)變量后,我們是無(wú)法對(duì)數(shù)據(jù)本身進(jìn)行修改的,重新綁定又是另一回事了,和修改數(shù)據(jù)本身無(wú)關(guān))
;;將列表綁定到lat上user=> (def lat (list 1 2 3))#'user/latuser=> lat(1 2 3);;我們得到的是一個(gè)新的列表user=> (cons 3 lat)(3 1 2 3);;原來(lái)列表并沒(méi)有改變user=> lat(1 2 3)
但是,有時(shí)候我們確實(shí)需要在數(shù)據(jù)被共享的時(shí)候去修改它。
事物型引用(Transactional References)
Clojure 提供了接口用于協(xié)調(diào)一個(gè)對(duì)象的并發(fā)修改。通過(guò)事務(wù)型引用(在clojure中對(duì)應(yīng)著Ref類型),clojure相當(dāng)于創(chuàng)建了一個(gè)管卡,每次只允許一個(gè)事務(wù)(類似關(guān)系數(shù)據(jù)庫(kù)中的事務(wù)概念)通過(guò)這個(gè)關(guān)卡,并且一個(gè)事務(wù)中的所有改變要么同時(shí)生效,要么回滾(這就是clojure軟件事務(wù)內(nèi)存STM的概念)。
包裝引用對(duì)象
我們可以使用ref函數(shù)講一個(gè)普通對(duì)象包裝成Ref類型對(duì)象
;;包裝一個(gè)空的哈希表=>(ref (hash-map))#<Ref@52879daa: {}>;;綁定一個(gè)Ref類型對(duì)象,該Ref包裝了一個(gè)哈希表=>(def vehicles (ref {:truck "Toyota" :car "Subaru" :plane "de Havilland"}))#'user/vehicles=>vehicles#<Ref@14325ad8: {:truck "Toyota", :car "Subaru", :plane "de Havilland"}>
解引用對(duì)象
接上面的例子,我們已經(jīng)有了一個(gè)被包裝在Ref類型之下的一個(gè)對(duì)象vehicles,但有很多函數(shù)需要訪問(wèn)被包裝的對(duì)象,而不是這個(gè)引用對(duì)象:
;; vehicles 不是一個(gè)map,所以會(huì)報(bào)錯(cuò)=>(keys vehicles)java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Ref =>(map? vehicles)false;;vehicles是Ref類型對(duì)象=>(class vehicles)clojure.lang.Ref
我們可以使用defref函數(shù)來(lái)獲取封裝在Ref內(nèi)部的對(duì)象,當(dāng)然還有一種更簡(jiǎn)潔的方式就是使用@符號(hào):=>(deref vehicles){:truck "Toyota", :car "Subaru", :plane "de Havilland"}=>@vehicles{:truck "Toyota", :car "Subaru", :plane "de Havilland"}=>(map? @vehicles)true=>(keys @vehicles)(:truck :car :plane)=>(vals @vehicles)("Toyota" "Subaru" "de Havilland")
修改引用對(duì)象
我們引用對(duì)象的目的,就是想修改它。之所以叫作事務(wù)型引用,是因?yàn)槲覀儽仨氃谝粋€(gè)事務(wù)中去修改它。在事務(wù)中(例如dosync)使用alter函數(shù)是最安全的修改方式,它能確保我們?cè)谑聞?wù)操作期間,其他對(duì)這個(gè)對(duì)象的改變都不會(huì)發(fā)生。
;;使用dosync可以理解成開(kāi)啟了一個(gè)事務(wù)=>(dosync (alter vehicles assoc :car "Volkswagon")){:truck "Toyota", :car "Volkswagon", :plane "de Havilland"};;vehicles是真的被改變了,而不是返回一個(gè)新的對(duì)象=>@vehicles{:truck "Toyota", :car "Volkswagon", :plane "de Havilland"};;使用alter刪除map的一個(gè)鍵值對(duì)=>(dosync (alter vehicles dissoc :car)){:truck "Toyota", :plane "de Havilland"};;修改在當(dāng)前對(duì)象上生效了=>@vehicles{:truck "Toyota", :plane "de Havilland"}
如果你不關(guān)心引用對(duì)象原來(lái)的值的話,可以使用ref-set來(lái)設(shè)置一個(gè)新的值
=>(dosync (ref-set vehicles {:motorcycle "Ducati"})){:motorcycle "Ducati"}=>vehicles#<Ref@229ec9cd: {:motorcycle "Ducati"}>
原子類型(Atoms)
和引用類型(Ref)類似,原子也是創(chuàng)建一個(gè)管卡來(lái)修改一個(gè)不可變對(duì)象,并且原子也能和Ref一樣進(jìn)行同步更新。但是原子并不要求在事務(wù)中運(yùn)行,并且它們不能協(xié)調(diào)多個(gè)狀態(tài)的更新。(多個(gè)Ref類型對(duì)象可以在一個(gè)事務(wù)中協(xié)調(diào)更新,要么同時(shí)成功,要么同時(shí)失敗,即回滾)
包裝原子對(duì)象
操作和ref類似
;;綁定一個(gè)原子對(duì)象,該原子對(duì)象包裝了一個(gè)哈希表=>(def vehicles (atom ["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]))#'user/vehicles=>vehicles#<Atom@31f3f16b: ["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]>
解引用原子對(duì)象
這個(gè)和ref一樣
=>(deref vehicles)["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]=>@vehicles["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]
修改原子對(duì)象
我們可以使用swap!函數(shù)或者reset!函數(shù)(名字后面有感嘆號(hào)在lisp方言中代表修改函數(shù)之意)來(lái)修改被原子包裝的對(duì)象。swap!用于在原來(lái)值的基礎(chǔ)上進(jìn)行修改,reset!則是直接用新值替換原來(lái)的值。
=>(swap! vehicles conj "Ducati Diavel")["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver" "Ducati Diavel"]=>@vehicles["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver" "Ducati Diavel"]=>(reset! vehicles (take 2 @vehicles))("Toyota Tacoma" "Subaru Outback")=>@vehicles("Toyota Tacoma" "Subaru Outback")
clojure 新手指南(16):基本迭代&遞歸 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/149922
迭代和遞歸是兩種不同的概念,但是它們彼此之間又有點(diǎn)相似。迭代是遍歷一組元素,并在遍歷的過(guò)程中對(duì)每一個(gè)元素做相應(yīng)的操作,遞歸則是執(zhí)行一個(gè)自己調(diào)用自己的操作。
從遞歸和迭代的概念上來(lái)看,這完全是兩種完全不同的東西,那么它們的相似性又體現(xiàn)在什么地方呢?首先遞歸也可以作為一種遍歷集合元素的方法,Clojure就有遞歸方式的迭代器。本章就是揭示clojure中迭代和遞歸的工作方式和使用它們的好處。
使用doseq進(jìn)行迭代
首先讓我們看一個(gè)示例問(wèn)題,我們最終需要使用迭代來(lái)解決這個(gè)問(wèn)題。我們的這個(gè)示例問(wèn)題被稱為FizzBuzz難題:
寫(xiě)一個(gè)程序打印1到100這些數(shù)字。但是遇到數(shù)字為3的倍數(shù)的時(shí)候,打印“Fizz”替代數(shù)字,5的倍數(shù)用“Buzz”代替,既是3的倍數(shù)又是5的倍數(shù)打印“FizzBuzz
讓我們開(kāi)始解決吧!從題目中分析知道,首先我們要確定1到100這些數(shù)字中可以整除3、整除5、既能整除3又能整除5的數(shù)字,所以至少需要一個(gè)判斷整除的函數(shù),我們不妨稱之為multiple?(以問(wèn)號(hào)結(jié)尾的函數(shù)名一般都返回布爾值)。我們可以利用clojure的內(nèi)置取余函數(shù)mod來(lái)創(chuàng)建我們的multiple?函數(shù)。
=>(defn multiple? [n div] ;; n 除以 div的余數(shù)是否等于0 (= 0 (mod n div)))#'user/multiple;; 3能被3整除,返回true=>(multiple? 3 3)true;;判斷4能被3整除,返回false=>(multiple? 4 3)false;;判斷5能被3整除,返回false=>(multiple? 5 3)false;;判斷6能被3整除,返回true=>(multiple? 6 3)true
現(xiàn)在我們已經(jīng)有了一個(gè)判斷整除的函數(shù)multiple?,可以開(kāi)始著手處理FizzBuzz問(wèn)題具體的處理了。在Clojure世界中,有多種方式可以遍歷元素。下面,我們將會(huì)使用'doseq'( 宏標(biāo)簽) 來(lái)做迭代操作。它會(huì)遍歷序列中的元素,并在遍歷過(guò)程中做相應(yīng)的處理。我們給doseq的第一個(gè)參數(shù)應(yīng)該是一個(gè)向量(vector),這個(gè)向量里包含一個(gè)綁定當(dāng)前元素的變量名(我們下面使用字母 i)和被遍歷的序列。doseq的第二個(gè)參數(shù)是一個(gè)操作表達(dá)式(s表達(dá)式),遍歷過(guò)程中將對(duì)每一個(gè)元素做處理。
先看一下一個(gè)簡(jiǎn)單的例子:
;;打印0到9的數(shù)字user=> (doseq [i (range 0 10)] (println i))0123456789nil
再來(lái)看看嵌套迭代(類似嵌套for循環(huán))
user=> (doseq [ x [1 2 3] y [1 2 3]] (println (* x y)))123246369nil
上面代碼和下面java代碼基本等價(jià)(所有的clojure表達(dá)式都是有返回值的,上面代碼中最后的nil就是返回值):
int [] array = {1, 2, 3};for(int i : array){ for(int j : array){ System.out.println( i * j ); }}
doseq介紹到此結(jié)束,我們來(lái)看如何使用doseq來(lái)解決我們的FizzBuzz問(wèn)題:=>(doseq [i (range 1 101)] ;;遍歷1到100 ;;首先判斷是否能同時(shí)被5和3整除 (cond (and (multiple? i 3)(multiple? i 5)) (println "FizzBuzz") ;;如果上面不滿足則判斷是否能被3整除 (multiple? i 3) (println "Fizz") ;;如果上面不滿足則判斷能否被5整除 (multiple? i 5) (println "Buzz") ;;否則直接打印數(shù)字值 :else (println i)))12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz省略.......
強(qiáng)調(diào)一點(diǎn):cond 的使用和if else非常像,cond按照從上到下的順序依次判斷表達(dá)式的真值,如果條件表達(dá)式真值為true,返回該條件表達(dá)式對(duì)應(yīng)的執(zhí)行表達(dá)式的值,然后此次判斷結(jié)束,否則會(huì)執(zhí)行下一條判斷語(yǔ)句,直至最終執(zhí)行到else語(yǔ)句。
;;cond 形式如下(cond (條件表達(dá)式1) (執(zhí)行表達(dá)式1) (條件表達(dá)式2) (執(zhí)行表達(dá)式2) ...... :else (執(zhí)行表達(dá)式n)) ;;cond 結(jié)束
使用for進(jìn)行迭代
for循環(huán)是另一種迭代的方式,但是接下來(lái)你會(huì)發(fā)現(xiàn)使用for循環(huán)不適合解決FizzBuzz問(wèn)題。for循環(huán)的語(yǔ)法和doseq是一樣的,只不過(guò)for 返回lazy seq(類似python 中的yield)而doseq是side effect。這么說(shuō)有點(diǎn)抽象,還是用例子來(lái)說(shuō)明吧:
;; 我們?cè)敕祷?-10中所有的偶數(shù),但是得到的結(jié)果是niluser=> (doseq [x (range 0 11) :when (even? x)] x)nil ;; 使用doseq只能返回nil,不夠我們可以在遍歷期間做其他事情。比如 打印user=> (doseq [x (range 0 10) :when (even? x)] (print x ","))0 ,2 ,4 ,6 ,8 ,nil ;; (nil 是整個(gè)式子的返回值,不要搞混了);;我們使用for來(lái)獲取0-10中所有的偶數(shù)user=> (for [x (range 0 10) :when (even? x)] x)(0 2 4 6 8)
可以這么說(shuō),使用doseq就向java中的for循環(huán),只能在循環(huán)過(guò)程中做些什么事情,而clojure中的for循環(huán)可以在每次的遍歷中向外輸出值,最終由這些值組成一個(gè)序列。
再用個(gè)例子體會(huì)一下
user=> (for [x [0 1 2 3 4 5] :let [y (* x 3)] :when (even? y)] y)(0 6 12) ;;我們得到的結(jié)果
for循環(huán)不適合解決FizzBuzz問(wèn)題的原因就在于,F(xiàn)izzBuzz只是在遍歷過(guò)程中需要打印出對(duì)應(yīng)的值,而不需要每次都返回結(jié)果。有興趣你可以把解決FizzBuzz代碼中的doseq換成for來(lái)看看輸出效果就明白了。
使用loop進(jìn)行遞歸
loop 在許多語(yǔ)言中都有這個(gè)關(guān)鍵字,基本上都是為了更好的使用迭代器而存在。但是在Clojure中,loop實(shí)際上是遞歸的,所以使用它需要更多一點(diǎn)的相關(guān)知識(shí)和代碼。
先看一下如何使用loop 來(lái)解決 FizzBuzz問(wèn)題,體會(huì)一下
(loop [data (range 1 101)] (if (not (empty? data)) (let [n (first data)] (cond (and (multiple? n 3)(multiple? n 5)) (println "FizzBuzz") (multiple? n 3) (println "Fizz") (multiple? n 5) (println "Buzz") :else (println n)) (recur (rest data)))))
首先cond里面的邏輯和之前doseq的一模一樣,這個(gè)是不變的。我們知道遞歸必須有一個(gè)結(jié)束條件,所以我們?cè)谶@里在遞歸開(kāi)始加入了一個(gè)判斷語(yǔ)句(if (not (empty? data)) ,就是判斷data是否為空列表,如果為空遞歸結(jié)束,否則繼續(xù)進(jìn)行。每次遞歸,我們都從列表中取出一個(gè)值,然后把它傳遞給cond那部分邏輯進(jìn)行判斷。cond邏輯結(jié)束后,為了能遞歸調(diào)用上面邏輯,我們使用recur來(lái)達(dá)到目的。上例中,我們每次都將使用本次遞歸中的列表除第一個(gè)元素以外的剩下列表進(jìn)行下一次遞歸。(遞歸必須是一個(gè)收斂的過(guò)程,否則遞歸將永遠(yuǎn)無(wú)法結(jié)束)我們使用loop來(lái)打印0-11的偶數(shù),對(duì)比之前的例子。主要體會(huì)如何使用遞歸思想來(lái)解決問(wèn)題
user=> (loop [x 0](when (<= x 10) ;;判斷遞歸是否結(jié)束的語(yǔ)句 (if (even? x) (println x)) (recur (+ x 1)))) ;;使用recur 向判斷結(jié)束方向收斂
(建議大家可以看看《the little schemer》,看完肯定能更好的掌握遞歸思想,并且對(duì)學(xué)習(xí)clojure大有好處)現(xiàn)在我們?cè)賮?lái)個(gè)稍微難點(diǎn)的例子,我們會(huì)遞歸迭代一組數(shù)字,然后搜集遍歷過(guò)程中得到的前十個(gè)偶數(shù)。注意這個(gè)例子和前面不同的是,我們每次遞歸(recur)傳入的參數(shù)是多個(gè),而不是一個(gè)。recur后面參數(shù)其實(shí)是和loop的第一個(gè)向量參數(shù)中的綁定參數(shù)(data、n、n-count、result)是一一對(duì)應(yīng)的,大家仔細(xì)觀察一下。
(loop [data (range 1 101) n (first data) n-count 0 result nil] ;; result 初始為空列表 (if (and n (< n-count 10)) ;;遞歸結(jié)束條件 (if (even? n) (recur (rest data) (first data) (inc n-count) (cons n result)) (recur (rest data) (first data) n-count result)) (reverse result))) ;;遞歸結(jié)束后,反轉(zhuǎn)結(jié)果列表
我們可以做的更好一點(diǎn),就是把上面定義成一個(gè)遞歸函數(shù):
=>(defn take-evens ;;我們定義的遞歸函數(shù) ([x nums](take-evens x nums 0 nil)) ;;參數(shù)模式一 ([x nums n-count result] ;;參數(shù)模式二 (if (empty? nums) ;;遞歸結(jié)束條件一 (reverse result) (if (< n-count x) ;;遞歸結(jié)束條件二 (let [n (first nums)] (if (even? n) (recur x (rest nums) (inc n-count) (cons n result)) (recur x (rest nums) n-count result))) (reverse result)))))#'user/take-evens
;;取出1到100中前十個(gè)偶數(shù)=>(take-evens 10 (range 1 101))(2 4 6 8 10 12 14 16 18 20);;取出1到100宗前5個(gè)偶數(shù)=>(take-evens 5 (range 1 101))(2 4 6 8 10)
Clojure惰性序列的頭保持問(wèn)題 - climbdream的個(gè)人空間 - 開(kāi)源中國(guó)社區(qū)
https://my.oschina.net/clopopo/blog/150130