Clojure之解構

jopen 8年前發布 | 58K 次閱讀 Clojure Lisp開發

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>

輸出結果如下:

nil

Hello, Clojure. 這是一個,下雨的季節...

Hello, Clojure. 這是一個,下雨的季節...</pre>

在宏定義里,我們在參數中設定一個固定參數作為條件判斷參數,解構出來的剩余參數部分全部作為代碼體。Clojure的宏異常強大,這個 unless 僅僅是小試牛刀而已。而且本篇文章的內容主要講解Clojure的解構特性,因此,對于宏的概念就無法過多涉及,希望后續能抽出時間再另行整理。

四. 結束

通過系統介紹強大的Clojure解構特性,相信大家領略到了Clojure的魅力和威力。Clojure經過若干年的發展與完善,得到了實踐的檢驗,已經積累了一定的成熟度,可作為Java語言之外的另一個選擇。而對于編程語言,可謂是“蘿卜青菜各有所愛”,只要能夠幫助你有效解決問題,就是你的壓箱利器。

來自: http://www.2gua.info/post/52

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!