Clojure

高階函式

一級函式

在函式程式設計中,函式是一級公民。這表示函式可以視為值。它們可以指派為值、傳遞到函式中,以及從函式中傳回。

在 Clojure 中,常見到使用 defn 定義函數,例如 (defn foo …​)。不過,這只是 (def foo (fn …​)) 的語法糖衣,fn 會傳回函數物件,而 defn 會傳回指向函數物件的變數。

高階函數

高階函數是一種函數,它

  1. 將一個或多個函數作為參數

  2. 傳回一個函數作為結果

這是任何語言中函數式程式設計的重要概念。

高階函數讓我們可以組合函數。這表示我們可以撰寫小型函數,並將它們組合起來建立較大的函數。就像將許多小型的樂高積木組合起來蓋房子一樣。

讓我們暫時離開理論,來看一個範例。

函數作為參數

我們來看兩個函數

(defn double-+
    [a b]
    (* 2 (+ a b)))
(defn double-*
    [a b]
    (* 2 (* a b)))

這些函數共用一個常見模式。它們只在名稱和用於計算 ab 的函數上有所不同。一般來說,模式如下所示。

(defn double-<f>
    [a b]
    (* 2 (f a b)))

我們可以透過將 f 傳入作為參數,來概化我們的 double- 函數。

(defn double-op
    [f a b]
    (* 2 (f a b)))

我們可以使用它來表達將運算結果加倍的概念,而不是必須個別撰寫執行特定加倍運算的函數。

函數文字

匿名函數是不帶名稱的函數。在 Clojure 中,可以使用兩種方式定義它們:fn 和文字 #(…​)。使用 defn 建立函數會立即將它繫結到名稱,而 fn 只會建立一個函數。

讓我們來看一個有關幾個樂團的範例

(def bands [
    {:name "Brown Beaters"   :genre :rock}
    {:name "Sunday Sunshine" :genre :blues}
    {:name "Foolish Beaters" :genre :rock}
    {:name "Monday Blues"    :genre :blues}
    {:name "Friday Fewer"    :genre :blues}
    {:name "Saturday Stars"  :genre :jazz}
    {:name "Sunday Brunch"   :genre :jazz}
])

我們只想擷取搖滾樂團。這是一個一次性的運算,我們不會在程式碼中的其他地方使用它。我們可以使用匿名函數來節省一些按鍵。

(def rock-bands
    (filter
        (fn [band] (= :rock (:genre band)))
        bands))

使用函數文字,我們可以更簡潔地定義 rock-bands,如下所示。

(def rock-bands (filter #(= :rock (:genre %)) bands))

函數文字支援透過 %%n%& 傳入多個參數。

#(println %1 %2 %3)

如果你正在撰寫匿名函數,文字語法很好用,因為它很精簡。不過,如果參數超過幾個,語法可能會難以閱讀。在這種情況下,使用 fn 可能會更合適。

傳回函數和閉包的函數

我們的第一個函數將稱為 adder。它將採用一個數字 x 作為其唯一參數並傳回一個函數。adder 傳回的函數也將採用一個數字 a 作為其參數並傳回 x + a

(defn adder [x]
  (fn [a] (+ x a)))

(def add-five (adder 5))

(add-five 100)
;; => 105

adder 傳回的函數是一個閉包。這表示它可以存取函數建立時作用域內的所有變數。add-five 可以存取 x,即使它在 adder 函數定義之外。

篩選

篩選是電腦程式設計中常見的操作。採用這組動物

(def pets [
    {:name "Fluffykins" :type :cat}
    {:name "Sparky" :type :dog}
    {:name "Tibby" :type :dog}
    {:name "Al" :type :fish}
    {:name "Victor" :type :bear}
])

我們想要篩選出非狗的動物,因為我們正在撰寫企業級軟體。首先,讓我們來看看一個正常的 for 迴圈。

(defn loop-dogs [pets]
    (loop [pets pets
           dogs []]
        (if (first pets)
            (recur (rest pets)
                   (if (= :dog (:type (first pets)))
                       (conj dogs (first pets))
                       dogs))
            dogs)))

這段程式碼運作良好,但它龐大且令人困惑。我們可以使用高階函數 filter 來簡化它。

(defn filter-dogs [pets]
    (filter #(= :dog (:type %)) pets))

使用 filter 的解決方案更清楚,並允許我們顯示意圖,而不仅仅是下達指令。我們可以將其分解成更小的部分,方法是將篩選函數分解成一個單獨的 var

(defn dog? [pet] (= :dog (:type pet)))

(defn filter-dogs [pets] (filter dog? pets))

原始作者:Michael Zavarella