之前提到,函式在 Clojure 中是一等公民,像資料一樣,可以當成參數傳遞給其它函式,或可以被當成返回值傳遞。而可以做到其中之一功能的函式便稱作高階函式 (Higher-order Function)。
Clojure 之中有許多函式都可以接受函式當作參數,例如 map
接受一個函式以及群集當作參數,它會遍歷群集中的各個元素,把每個元素套用到當作參數的函式中,套用後的各個返回值再放到新的序列裡。這種功能稱作「映射」。
以下的例子示範了利用 map
函式,將向量中的各個字串,利用 clojure.string/lower-case
函式轉成小寫:
(map clojure.string/lower-case ["White" "Black" "Red"])
;; => ("white" "black" "red")
以上的範例相當於對每個元素呼叫 clojure.string/lower-case
:
[(clojure.string/lower-case "White") (clojure.string/lower-case "Black") (clojure.string/lower-case "Red")]
除了「映射」之外還有「化約」功能的 reduce
函式。reduce
如同 map
一樣接受函式與群集當作參數,它會遍歷群集中每個元素,套用到當作參數的函式。每次一個元素套用函式之後的結果,將會與下一個元素一起套用到當作參數的函式中。
以下範例示範如何使用 reduce
計算出群集中所有元素的和:
(reduce + [1 2 3 4 5])
;; => 15
以上的範例相當於先計算出 1 + 2 的結果,再將結果加上 3、加上 4、最後加上 5:
(+ (+ (+ (+ 1 2) 3) 4) 5)
;; => 15
利用 filter
函式則可以依據當作參數的函式其中的條件,來決定新的序列中究竟要放上什麼元素:
(filter #(> % 5) [2 3 5 10 15])
;; => (10 15)
filter
遍歷群集中的元素,將每個元素各別代入到 #(> % 5)
匿名函式中,匿名函式中判斷是否大於 5。只要函式返回值是真,filter
便將元素保留,否則剔除。因此新的序列裡只留下大於 5 的元素。
filter
接受的函式返回布林值,這種函式被稱爲「述詞函式」(Predicate),命名習慣上會在名稱後加上問號 (?),以表明它的返回值不是真便是假。even?
函式如果接受到偶數則返回真,反之則否,以下範例將奇數剔除,只留下偶數:
(filter even? [2 3 4 5 6])
;; => (2 4 6)
some
則是接受述詞函式與一個群集,遍歷群集中的元素並逐個丟給述詞函式,只要遇到元素讓述詞函式返回真,some
則返回真,反之則返回 nil
:
(some #(> % 5) [1 3 5 7 9])
;; => true
(some nil? [1 3 5 7 9])
;; => nil
以上範例分別示範了檢查群集中是否有大於 5 的元素,以及是否有 nil 元素在其中。
every?
函式接受一個述詞函式和群集,只有群集中的每個元素都讓述詞函式返回真,every?
函式才會返回真,反之則否。以下範例示範群集中的各個元素是否皆爲偶數:
(every? even? [1 2 3 4 5])
;; => false
(every? even? [2 4 6 8 10])
;; => true
高階函式的另一個特色是可以返回一個函式當作結果,Clojure 提供了一些函式協助將一群函式組合成另一個函式返回。其中 comp
接受一群函式作爲參數,並返回新的函式,由右至左地呼叫傳入的函式。以下範例示範以組合的方式實作將字串中的空白去除,並將第一個字母改成大寫:
(def cap-without-space (comp clojure.string/capitalize clojure.string/trim))
(cap-without-space " clojure ")
;; => "Clojure"
或是定義一個取出序列中第四個元素的函式:
(def fourth (comp first rest rest rest))
(fourth [1 2 3 4 5])
;; => 4
partial
函式則是建立一個缺少的函式,缺少的是剩下的參數,通常使用在剩下的參數並不清楚的時候。以下的範例使用了 partial
建立了會加 5 的函式,由於產生的函式尚未完備,必須等剩下的參數補齊才會產生結果:
(def plus5 (partial + 5))
(plus5 2)
;; => 7
(plus5 10)
;; => 15
最後要討論的是 complement
函式,這個函式接受一個返回值是布林的函式,返回它相反的布林值:
((complement even?) 2)
;; => false
((complement true?) false)
;; => true
除了使用 comp
、partial
、complement
生成新函式的函式之外,當然也可以寫自己的函式來生成新函式。以下的範例演示了一個生成函式的函式,它接受一個參數後返回一個函式,以此參數來相加後續代入的參數:
(defn adder [x]
(fn [y] (+ x y)))
(def adder5 (adder 5))
(adder5 3)
;; => 8
(adder5 10)
;; => 15
以上的範例除了示範了返回函式,返回的函式還將創建時帶入到父函式的參數記住,供以後使用。這種函式被稱作閉包 (Closure)。
有個與函式相關的祕訣:向量、映射與集合也可以當作函式來使用:
([1 3 5 7] 2)
;; => 2
(#{1 2 3} 1)
;; => 1
({:a 1 :b 2 :c 3} :c)
;; => 3
向量當成函式時,參數就是索引值;映射當成函式時,參數就是索引鍵;集合當成函式時,參數就是集合中的內容,當參數並不在集合中則回傳 nil
。
若是將它們與高階函式一起使用,就可以產生簡潔的應用。以下範例使用 remove
函式,第一個參數是述語函式,用集合來當作述語函式。範例中,集合的內容是不受歡迎的賓客名字,第二個參數則是賓客名單,運算之後產生去除不受歡迎的賓客名單:
(def banned #{"Steve" "Michael"})
(def guest-list ["Brian" "Josh" "Steve"])
(remove banned guest-list)
;; => ("Brian" "Josh")
或是使用 map
函式將向量中的特定元素抽取出來:
(map [:a :b :c :d :e] #{0 3})
;; => (:a :d)
(未完待續)