Clojure
零基礎
學習筆記
遍歷
map
filter
reduce
匿名函數
體驗聲明式[1]的 “自動化” 遍歷
遍歷是一個非常常見的需求,我們經常需要把一個集合中的每一個元素都取出來搗鼓點什么。這次我們來介紹一下函數式世界里非常實用的幾個函數,它們能非常方便地處理這些遍歷問題:
- 依次把集合中的每一個元素取出來執行某種操作。
- 找出集合中所有滿足某一指定條件的元素。
- 依次取出元素,進行一系列操作后,把這次操作返回值和下一個元素一起,再次進行操作...直至最后一個元素。
解決第一個問題的函數是我們已經見過的 map
函數,它能把集合中的元素依次取出作為指定函數的參數,并把每次執行的返回值以列表形式返回。
第二個“篩選”問題,我們使用 filter
函數來解決,我們會在接下來的內容中來一起認識一下這個新伙伴。
前兩個問題比較容易理解,第三個問題看起來比較麻煩, reduce
函數可以用來處理這種問題,這個過程稱為“規約”,我們馬上就會了解到如何來使用它。
首先我們來和們的老朋友 map
函數打個招呼。我們來看看如何使用它來把一個數字集合中所有的數字都加上 1:
=> (defn plus-one
[x]
(+ x 1))
#'user/plus-one
=> (map plus-one [41 443 24346 23 54 3 35])
(42 444 24347 24 55 4 36)
; 事實上 Clojure 已經內置了函數 inc,
; 它與我們自己實現的 plus-one 函數在功能上一模一樣
=> (map inc [41 443 24346 23 54 3 35])
(42 444 24347 24 55 4 36)
非常的簡便,如果你想進行其他操作,只需修改map
函數的第二個參數。
比如,我們想操作某個保存“用戶信息”的復合數據結構,把出生月份在9月份之前的用戶年齡增加 1:
=> (map (fn [map-person-info]
(if (< (:birthmonth map-person-info) 9)
(assoc map-person-info :age (inc (:age map-person-info)))
map-person-info))
[{:name "sun" :birthmonth 12 :age 24} {:name "li" :birthmonth 5 :age 20}])
({:name "sun", :birthmonth 12, :age 24} {:name "li", :birthmonth 5, :age 21})
注意這里我們使用了匿名函數 fn
。當你想使用這種使用一次就丟棄的“一次性”函數時,就可以考慮使用匿名函數。
不過,Clojure 還提供了一種更為炫酷的匿名函數形式,它看起來是這樣子的:
#(+ % 1)
;上面的形式等價于下面的形式
(fn [some-num]
(+ some-num 1))
不難看出,其實這種形式就是把參數列表和參數名用 %
來代替,然后直接在 #()
里填寫函數體。如果有多個參數,就以 %1
%2
... 來代替。
所以上面的給用戶年齡加一的例子使用精簡版匿名函數來寫,看起來就會是這個樣子:
(map #(if (< (:birthmonth %) 9)
(assoc % :age (inc (:age %)))
%)
[{:name "sun" :birthmonth 12 :age 24} {:name "li" :birthmonth 5 :age 20}])
不過要注意,匿名函數的語法糖形式不可嵌套!!!而 fn
則可以嵌套。一個原因是,如果你使用非常炫酷的 #()
進行了過多層次的嵌套,可能連你自己也讀不懂。另一個重要原因是,很難去區別處理 %
到底是屬于外層還是內層。
它們之間還有一些不同,鑒于篇幅,你可以自行查閱相關文檔來了解。
現在出場的是 filter
函數,正如同它的名字“過濾”,我們可以使用它進行方便的過濾工作。
比如我們要過濾數字集合中大于 8 的數字:
=> (filter #(> % 8) [3 5 426 676 55475 12 4 78 2 48])
(426 676 55475 12 78 48)
filter
函數的使用方法也很簡單,它的第一個參數是一個返回值類型是布爾型(boolean)的函數(也就是返回值是 true 或者 false 的函數),第二個參數是待過濾的集合。filter
函數會一一檢查集合中的元素是否滿足條件,即依次取出集合中的元素作為我們提供的布爾型函數的參數,把結果為 true 的元素留下。
再來看一個例子,找到 1 到 10 之間的奇數:
=> (defn odd-number? ; Clojure 里把返回布爾型的函數命名為 xx? 的形式
[number]
(not= (mod number 2) 0)) ; 除以2余數不為0的數字即為奇數
#'user/odd-number?
=> (filter odd-number? (range 1 11))
(1 3 5 7 9)
; 事實上 Clojure 已經提供了 odd? 函數,和我們自己實現的版本功能上一樣
=> (filter odd? (range 1 11))
(1 3 5 7 9)
如果加強一點,還可以找到質數:
=> (defn prime?
[number]
(empty? (filter #(= 0 (mod number %)) (range 2 number))))
#'user/prime?
=> (filter prime? (range 1 101))
(1 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)
當然我們這個函數的速度還有很大的進步空間,進一步優化就交給有興趣的同學了。
最后我們來看看 reduce
函數,文字上很難描述它的功能,不過看了它的例子之后會發現它也是很容易的,這個例子是簡單的加法:
=> (reduce + [1 2 3 4 5])
15
雖然我們的 +
函數支持多個參數,但是我們假設加法函數只支持兩個數字的加法,那么就需要使用 reduce
函數來完成多參數的加法了。它的執行過程是這樣的:
- 首先取出集合的前兩個元素作為
+
的參數,執行函數得到返回值 3 - 然后把上一步驟得到的返回值 3,和集合的第三個元素 3,作為
+
的參數,執行函數得到返回值 6 - 然后把上一步驟得到的返回值 6,和集合的第四個元素 4,作為
+
的參數,執行函數得到返回值 10 - ...
- 直到集合中的所有元素都被處理,返回最終返回值 15。
這種把上一次的結果和下一個元素作為接下來的函數參數,重復直到遍歷元素的過程,稱為“規約”。
由于這種特性,它第二個參數接受的函數必須支持傳遞兩個參數。
我們還可以給規約過程提供一個初始值,比如下面這個例子,可以把一個集合中的元素添加進另一個集合中:
=> (reduce conj [1 3] [1 2 3])
[1 3 1 2 3]
這個例子中,[1 3]
是初始值,第一次執行會從 [1 2 3]
中取出第一個元素 1 ,通過 conj
添加進 [1 3]
中,得到結果 [1 3 1]
,以此類推,最終結果是 [1 3 1 2 3]
。
最后總結,在函數式語言中,遍歷是聲明式的,你無需控制遍歷過程,只需使用相應的高階函數,往高階函數中傳遞不同的函數,再通過函數之間靈活自由的組合,即可輕松應對。
實際上,往往需要把本次介紹的函數結合起來使用,以此應對更為復雜的問題。
比如先使用 map
進行初步處理,再使用 filter
過濾,最后使用 reduce
規約,得到最終結果。
-
聲明式編程:告訴程序你想要的是什么,剩下的交給程序來自動處理。命令式編程:一步一步的命令程序如何操作,程序會按照你的命令去進行操作。 ?