Clojure之解構
Clojure之解構
2016.01.14 09:52:29
這篇算是比較完整的Clojure解構用法文章了。
Clojure是一門Lisp方言——確切地說,是一門JVM上的Lisp方言——也是一門非純粹的函數式語言。從我對Clojure的認知來看,Clojure的掌握成本并不低,但也不算太高。況且現在說自己簡單的編程語言,深究起來有哪一門是簡單的呢?Python的坑其實也蠻多的嘛?!
Clojure理所當然地秉承了Lisp“代碼即數據( code is data! )”的設計哲學,直接面向抽象語法樹( abstract syntax tree ,AST)。該特性正是讓無數熟諳其它語言模式的開發者難以跨越的一道門檻。但無可否認的是,Lisp、Clojure中這個獨樹一幟的著名特性,內含了無窮無盡魔法威力,并通過括號體現出強大的語言表現能力。同Lisp一樣,Clojure為數據操作的方便性提供了眾多支撐手段, 解構 ( Destructuring )特性就是其中之一,為提高編程效率助力不少。
一. 解構的提出
先來看一個例子,一個Vector: ["Mary" "Vivian" "David"] ,可以按如下方式取值:
(let [v ["Mary" "Vivian" "David"]] (vector (first v) (second v) (last v) (nth v 1) (nth v 2)))
看到了吧?各種的 first 、 second 、 last 、 nth 在大量使用的情況下,可想而知是如何繁瑣。
Python里可以這樣:
mary, vivian, david= ["Mary", "Vivian", "David"]
那么,Clojure中是否也有類似的簡潔而高效的Hack用法呢?
當然。這個特性在Clojure中稱為 解構 ( Destructuring ,Python、Ruby中稱為 Unpacking ):
(let [[mary vivian david] ["Mary" "Vivian" "David"]] (vector mary vivian david))
解構涉及的另一個核心概念是 綁定 ( binding ),如mary綁定到 ["Mary" "Vivian" "David"] 的"Mary"處位置,綁定概念是解構特性的基礎。
實際上,Clojure的解構特性會來得更加強大而豐富,堪稱摸金校尉手中的飛虎爪,最是擅長“隔空取物”。接下來就來系統了解一下。
二. 解構特性
2.1 序列集合的解構
1. 嵌套序列解構
除了上面提到的最基本示例,Clojure的解構同時還支持嵌套序列的處理:
(let [[mary vivian david [foo1 foo2 [bar1 bar2]]] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]] (vector mary vivian david [foo1 foo2 [bar1 bar2]]))
得到結果:
["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]
2. 序列分解方式
如果我們只關心解構序列中的某部分內容,可以考慮以下方式:
(let [[mary _ _ [foo1 _ [bar1 _]]] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]] (vector mary [foo1 [bar1]]))
結果為:
["Mary" ["foo1" ["bar1"]]]
這時候,只獲取非下劃線 “_” 位置上符號綁定的內容(下劃線 “_” 在這里代表的意思是:“這個位置上確實有東西,但是是什么東西我不關心,你盡管把東西放到這個位置上”)。這種分而待之的方式進一步增強了Clojure解構特性的能力。
此外,還可以通過 “&” 將序列分解為前部分與剩余部分:
(let [[mary _ david & more] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]] (println more))
輸出如下: ([foo1 foo2 [bar1 bar2]])
3. 保留原始內容
有時候我們還希望保留原始的序列集合,這時候應該怎么辦呢?
(let [[mary _ david :as original] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]] (println "mary:" mary "," "original:" original))
我們可以通過關鍵字 :as 來將原始內容綁定到本地符號上。結果:
mary: Mary , original: [Mary Vivian David [foo1 foo2 [bar1 bar2]]]
4. 字符串解構
解構對于字符串同樣合適,這也大大方便了字符串的操作:
(let [[initial] "Mary"] (println "initial:" initial))
結果:
initial: M
上述我們的示例大部分針對的是Vector的解構,但實際上,解構對整個序列集合也是適用的:
(let [[mary vivian david] '("Mary" "Vivian" "David")] (vector mary vivian david))
2.2 Map解構
毫無疑問,Map在開發過程中是應用較多的數據結構,Clojure賦予Map的解構特性,使得Map的發揮如虎添翼。
(let [{mary :mary, vivian :vivian, david :david} {:mary "Mary", :vivian "Vivian", :david "David"}] (println mary vivian david))
這種方式雖然是解構,但一點也沒提高我們的效率,還得寫不少累贅的東西。Clojure提供了貼心的鍵與關鍵字映射的方式,我們可以通過它來進一步簡化我們的代碼。
1. 鍵與關鍵字映射
我們把上述例子改一改:
(let [{:keys [mary vivian david]} {:mary "Mary", :vivian "Vivian", :david "David"}] (println mary vivian david))
輸出結果:
Mary Vivian David
Clojure提供了關鍵字 :keys ,用來在解構過程中聰明地將提取符號與原始Map的鍵一一對應起來,這樣我們就少寫了許多代碼,在原始Map很復雜的情況下,無疑將大大提高開發效率。
除了關鍵字 :keys ,Clojure還提供了其他關鍵字: :strs 、 :syms ,用于對應Map中不同的Key類型。事實上,這是一種快捷方式。我們再來看個例子:
(let [{:keys [mary vivian david], :strs [changkong], :syms [luohan]} {:mary "Mary", :vivian "Vivian", :david "David", 'luohan "少林羅漢拳", "changkong" "長空劍法"}] (println mary vivian david luohan changkong))
輸出結果:
Mary Vivian David 少林羅漢拳 長空劍法
此外在這里要注意,綁定中的方括號 “[]” 不可少,即使只有一個綁定的符號,如: :strs [changkong] 。同時,綁定符號的字面也要跟原Map中Key類型(關鍵字、字符串或符號)字面值一致,如 :syms [luohan] 中的 luohan ,跟Map中的 'luohan "少林羅漢拳" 的 luohan 相對應。
2. :as 與 :or
:as 在Map中同樣適用。
(let [{:keys [mary vivian david], :as original} {:mary "Mary", :vivian "Vivian", :david "David", 'luohan "少林羅漢拳", "changkong" "長空劍法"}] (println mary vivian david original))
結果如下:
Mary Vivian David {:mary Mary, :vivian Vivian, :david David, luohan 少林羅漢拳, changkong 長空劍法}
此外,解構應用于Map的特性中還有一個關鍵字 :or ,用來指定綁定符號的缺省值,如果找不到綁定符號對應Map中的鍵時,則使用該缺省值。
(let [{:keys [mary vivian david tom] :or {tom "not found"}} {:mary "Mary", :vivian "Vivian", :david "David"}] (println mary vivian david tom))
輸出如下:
Mary Vivian David not found
3. 復雜嵌套Map的解構
Map在日常開發中的使用是如此廣泛,因此,有必要來了解一下復雜嵌套Map的解構過程。
先來看一個例子:
(let [{mary :mary, {first-name :first-name, last-name :last-name} :name} {:mary "Mary" :name {:first-name "Tom", :last-name "Hanks"}}] (format "%s和%s.%s。" mary first-name last-name))
這段代碼解構了一個嵌套的Map:name,結果如下:
"Mary和Tom.Hanks。"
再稍微深入一點,如果我們想結合 :keys 或 :strs 、 :syms 來操作嵌套Map,要如何操作?哈哈哈,你可以停下來想一想。嗯哼,我們可以這樣操作(以 :keys 為例子):
(let [{:keys [mary name]} {:mary "Mary" :vivian "Vivian" :name {:first-name "Tom", :last-name "Hanks"}} {:keys [first-name last-name]} name] (println (format "%s和%s.%s。" mary first-name last-name)))
結果跟上面是一樣的。當然,我覺得用 :keys 的方式會更優雅一些,但這也要看個人口味。
4. Map解構與Vector、字符串
Map解構也可應用于Vector、字符串。此時,Vector、字符串的下標索引可以在Map解構形式( Form )中作為其自身的Key來使用。
(let [{c0 0, c1 1} "Hello, Clojure."] (println (format "c1和c2分別為:%c、%c。" c0 c1)))
結果:
c1和c2分別為:H、e。
對Vector的操作也是一樣。但有一點需要特別點出,那就是可以通過 & 獲取Vector的剩余部分,并在剩余部分元素個數為偶數時,Map解構特性會自動將剩余部分當作Map來看待!這個實現其實并不新奇,原理跟 array-map 和 assoc 是一樣的,來看看:
(let [[m v luohan & {:syms [changkong]}] ["Mary" "Vivian" "少林羅漢拳" 'changkong "長空劍法"]] (format "%s和%s使的是%s、%s。" m v changkong luohan))
結果如下:
"Mary和Vivian使的是長空劍法、少林羅漢拳。"
我們不妨把"Vivian"放到剩余部分,看看會發生什么:
(let [[m & {:strs [Vivian], :syms [changkong]}] ["Mary" "Vivian" "少林羅漢拳" 'changkong "長空劍法"]] (println (format "Vivian變成了%s。長空劍法還是%s。" Vivian, changkong)))
看看輸出內容:
Vivian變成了少林羅漢拳。長空劍法還是長空劍法。
這種感覺真有點難以名狀?
5. 其他
自Clojure 1.6起,可以在Map解構形式中使用帶前綴的Map Key(中文亮了):
(let [{:keys [拳法/luohan 劍法/changkong]} {:拳法/luohan "少林羅漢拳", :劍法/changkong "長空劍法"}] (println (format "%s,%s" luohan changkong)))
輸出:
少林羅漢拳,長空劍法
三. 解構特性的應用
解構特性于Clojure的函數與宏定義而言意義重大。我們先來看看 解構在函數方面的示例 :
(defn kong-fu [name school & {:keys [quan jian]}] (printf "%s來自%s,以%s、%s聞名。\n" name school quan jian))(kong-fu "胡八兒" "華山派" :quan "少林羅漢拳", :jian "長空劍法")</pre>
結果:
胡八兒來自華山派,以少林羅漢拳、長空劍法聞名。在 kong-fu 函數中,參數使用到Map解構,并利用了前面提到的:通過 & 獲取剩余部分,當剩余部分元素為偶數個時,將剩余部分當作Map來看待的特性。
再來看個不定長參數的例子:
(defn variable-params [& more] (printf "參數為:%s。\n" more))(variable-params 1 2 3 4 5 "Mary" "Vivian" "David") (variable-params) (variable-params 1 10 120)</pre>
輸出結果:
參數為:(1 2 3 4 5 "Mary" "Vivian" "David")。 參數為:null。 參數為:(1 10 120)。在這里,我們把參數統一設置為剩余部分,這樣就達到了不定長參數的目的,然后在函數中還可以對參數進一步解構。
接下來,我們再來了解一下強大的 宏定義中解構的應用 。實現一個 unless 函數:如果測試條件為false,則執行代碼體。
(defmacro unless "如果test為false,則執行代碼體。" {:added "1.0"} [test & body] (list 'if-not test (cons 'do body)))(unless true (println "Hello, Clojure.") (println "這是一個,下雨的季節...")) (unless false (println "Hello, Clojure.") (println "這是一個,下雨的季節...")) (unless (= "下雨" "出太陽") (println "Hello, Clojure.") (println "這是一個,下雨的季節..."))</pre>
輸出結果如下:
nilHello, Clojure. 這是一個,下雨的季節...
Hello, Clojure. 這是一個,下雨的季節...</pre>
在宏定義里,我們在參數中設定一個固定參數作為條件判斷參數,解構出來的剩余參數部分全部作為代碼體。Clojure的宏異常強大,這個 unless 僅僅是小試牛刀而已。而且本篇文章的內容主要講解Clojure的解構特性,因此,對于宏的概念就無法過多涉及,希望后續能抽出時間再另行整理。
四. 結束
通過系統介紹強大的Clojure解構特性,相信大家領略到了Clojure的魅力和威力。Clojure經過若干年的發展與完善,得到了實踐的檢驗,已經積累了一定的成熟度,可作為Java語言之外的另一個選擇。而對于編程語言,可謂是“蘿卜青菜各有所愛”,只要能夠幫助你有效解決問題,就是你的壓箱利器。
![]()