Clojure

高阶函数

一等函数

在函数式编程中,函数是一等公民。这意味着函数可以像值一样对待。它们可以作为值赋值,传递给函数,并从函数中返回。

在 Clojure 中,使用 defn 定义函数很常见,例如 (defn foo …​)。但是,这只是 (def foo (fn …​)) 的语法糖。fn 返回一个函数对象。defn 返回一个指向函数对象的 var。

高阶函数

高阶函数是一个函数,它:

  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