iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
0
Software Development

擁抱 Clojure系列 第 9

[第 09 天] 擁抱 Clojure:繫結與函式(三)

繫結與函式(三)

函式

高階函式

之前提到,函式在 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

除了使用 comppartialcomplement 生成新函式的函式之外,當然也可以寫自己的函式來生成新函式。以下的範例演示了一個生成函式的函式,它接受一個參數後返回一個函式,以此參數來相加後續代入的參數:

(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)

(未完待續)


上一篇
[第 08 天] 擁抱 Clojure:繫結與函式(二)
下一篇
[第 10 天] 擁抱 Clojure:繫結與函式(四)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言