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