Clojure 學習筆記 :8 遍歷元素

Clojure 零基礎 學習筆記 遍歷 map filter reduce 匿名函數


體驗聲明式[1]的 “自動化” 遍歷

遍歷是一個非常常見的需求,我們經常需要把一個集合中的每一個元素都取出來搗鼓點什么。這次我們來介紹一下函數式世界里非常實用的幾個函數,它們能非常方便地處理這些遍歷問題:

  1. 依次把集合中的每一個元素取出來執行某種操作。
  2. 找出集合中所有滿足某一指定條件的元素。
  3. 依次取出元素,進行一系列操作后,把這次操作返回值和下一個元素一起,再次進行操作...直至最后一個元素。

解決第一個問題的函數是我們已經見過的 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 函數來完成多參數的加法了。它的執行過程是這樣的:

  1. 首先取出集合的前兩個元素作為 + 的參數,執行函數得到返回值 3
  2. 然后把上一步驟得到的返回值 3,和集合的第三個元素 3,作為 + 的參數,執行函數得到返回值 6
  3. 然后把上一步驟得到的返回值 6,和集合的第四個元素 4,作為 + 的參數,執行函數得到返回值 10
  4. ...
  5. 直到集合中的所有元素都被處理,返回最終返回值 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 規約,得到最終結果。


  1. 聲明式編程:告訴程序你想要的是什么,剩下的交給程序來自動處理。命令式編程:一步一步的命令程序如何操作,程序會按照你的命令去進行操作。 ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容