Clojure
零基礎
學習筆記
遍歷
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ù)。
比如掂之,我們想操作某個保存“用戶信息”的復合數(shù)據(jù)結構抗俄,把出生月份在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
。當你想使用這種使用一次就丟棄的“一次性”函數(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ù)的語法糖形式不可嵌套6刘巍!禾唁!而 fn
則可以嵌套效览。一個原因是,如果你使用非常炫酷的 #()
進行了過多層次的嵌套荡短,可能連你自己也讀不懂丐枉。另一個重要原因是,很難去區(qū)別處理 %
到底是屬于外層還是內(nèi)層掘托。
它們之間還有一些不同瘦锹,鑒于篇幅,你可以自行查閱相關文檔來了解闪盔。
現(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ù),把結果為 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)
當然我們這個函數(shù)的速度還有很大的進步空間,進一步優(yōu)化就交給有興趣的同學了澎胡。
最后我們來看看 reduce
函數(shù)孕荠,文字上很難描述它的功能,不過看了它的例子之后會發(fā)現(xiàn)它也是很容易的攻谁,這個例子是簡單的加法:
=> (reduce + [1 2 3 4 5])
15
雖然我們的 +
函數(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。
這種把上一次的結果和下一個元素作為接下來的函數(shù)參數(shù)米绕,重復直到遍歷元素的過程瑟捣,稱為“規(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]
中芙扎,得到結果 [1 3 1]
,以此類推填大,最終結果是 [1 3 1 2 3]
戒洼。
最后總結,在函數(shù)式語言中允华,遍歷是聲明式的圈浇,你無需控制遍歷過程,只需使用相應的高階函數(shù)靴寂,往高階函數(shù)中傳遞不同的函數(shù)磷蜀,再通過函數(shù)之間靈活自由的組合,即可輕松應對百炬。
實際上褐隆,往往需要把本次介紹的函數(shù)結合起來使用,以此應對更為復雜的問題剖踊。
比如先使用 map
進行初步處理庶弃,再使用 filter
過濾,最后使用 reduce
規(guī)約德澈,得到最終結果歇攻。
-
聲明式編程:告訴程序你想要的是什么,剩下的交給程序來自動處理梆造。命令式編程:一步一步的命令程序如何操作缴守,程序會按照你的命令去進行操作。 ?