Clojure

函數式程式設計

Clojure 是一種函數式程式設計語言。它提供避免可變狀態的工具,提供函數作為一級物件,並強調遞迴迭代,而不是基於副作用的迴圈。Clojure 是「不純粹」的,因為它不會強制程式為參考透明,也不會追求「可證明」的程式。Clojure 背後的哲學是,大多數程式的大部分都應該是函數式的,而函數式程度較高的程式更為強健。

一級函數

fn 建立函數物件。它會產生一個像其他值一樣的值 - 你可以將它儲存在變數中,傳遞給函數等。

(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"

defn 是巨集,讓定義函數變得更簡單。Clojure 支援在單一函數物件中進行元數超載、自我參照和使用 & 的可變元數函數

;trumped-up example
(defn argcount
  ([] 0)
  ([x] 1)
  ([x y] 2)
  ([x y & more] (+ (argcount x y) (count more))))
-> #'user/argcount
(argcount)
-> 0
(argcount 1)
-> 1
(argcount 1 2)
-> 2
(argcount 1 2 3 4 5)
-> 5

你可以使用 let 在函數內部為值建立區域名稱。任何區域名稱的範圍都是詞彙的,因此在區域名稱範圍內建立的函數將封閉其值

(defn make-adder [x]
  (let [y x]
    (fn [z] (+ y z))))
(def add2 (make-adder 2))
(add2 4)
-> 6

使用 let 建立的區域變數不是變數。一旦建立,其值就永遠不會改變!

不可變資料結構

避免變異狀態最簡單的方法就是使用不可變的資料結構。Clojure 提供一組不可變的清單、向量、集合和映射。由於它們無法被變更,從不可變集合中「新增」或「移除」某個項目表示建立一個新集合,它就像舊集合一樣,但具有必要的變更。持久性是一個用來描述集合的舊版本在「變更」後仍然可用的屬性的術語,而且集合對大多數作業維持其效能保證。具體來說,這表示無法使用完整副本建立新版本,因為那需要線性時間。不可避免地,持久集合是使用連結資料結構實作的,以便新版本可以與先前版本共用結構。單連結清單和樹是基本的函數式資料結構,Clojure 在此基礎上新增一個雜湊映射、集合和向量,它們都基於陣列對應雜湊嘗試。這些集合具有可讀的表示形式和共用介面

(let [my-vector [1 2 3 4]
      my-map {:fred "ethel"}
      my-list (list 4 3 2 1)]
  (list
    (conj my-vector 5)
    (assoc my-map :ricky "lucy")
    (conj my-list 5)
    ;the originals are intact
    my-vector
    my-map
    my-list))
-> ([1 2 3 4 5] {:ricky "lucy", :fred "ethel"} (5 4 3 2 1) [1 2 3 4] {:fred "ethel"} (4 3 2 1))

應用程式通常需要關聯屬性和其他資料,這些資料與資料的邏輯值正交。Clojure 直接支援這個元資料。符號和所有集合都支援元資料映射。可以使用meta 函數存取它。元資料不會影響相等語意,而且元資料不會顯示在對集合值的作業中。元資料可以讀取,也可以列印。

(def v [1 2 3])
(def attributed-v (with-meta v {:source :trusted}))
(:source (meta attributed-v))
-> :trusted
(= v attributed-v)
-> true

可擴充抽象

Clojure 使用 Java 介面來定義其核心資料結構。這允許 Clojure 擴充至這些介面的新具體實作,而且函式庫函數將會使用這些擴充。與將語言硬連線至其資料類型的具體實作相比,這是一個很大的進步。

一個很好的範例是seq 介面。透過將核心 Lisp 清單建構轉換為抽象,大量的函式庫函數會擴充至任何可以提供其內容的順序介面的資料結構。所有 Clojure 資料結構都可以提供 seq。seq 可以像其他語言中的反覆運算器或產生器一樣使用,其顯著優點在於 seq 是不可變的且持久的。seq 非常簡單,提供一個first 函數,它會傳回順序中的第一個項目,以及一個rest 函數,它會傳回順序的其餘部分,而其本身是一個 seq 或 nil。

(let [my-vector [1 2 3 4]
      my-map {:fred "ethel" :ricky "lucy"}
      my-list (list 4 3 2 1)]
  [(first my-vector)
   (rest my-vector)
   (keys my-map)
   (vals my-map)
   (first my-list)
   (rest my-list)])
-> [1 (2 3 4) (:ricky :fred) ("lucy" "ethel") 4 (3 2 1)]

許多 Clojure 函式庫函式會延遲產生和使用 seq

;cycle produces an 'infinite' seq!
(take 15 (cycle [1 2 3 4]))
-> (1 2 3 4 1 2 3 4 1 2 3 4 1 2 3)

你可以使用 lazy-seq 巨集定義你自己的延遲 seq 產生函式,它會採用一個表達式主體,在需要時呼叫它來產生一個包含 0 個或更多項目的清單。以下是簡化的 take

(defn take [n coll]
  (lazy-seq
    (when (pos? n)
      (when-let [s (seq coll)]
       (cons (first s) (take (dec n) (rest s)))))))

遞迴迴圈

在沒有可變動區域變數的情況下,迴圈和反覆運算必須採用與內建 forwhile 建構的語言不同的形式,這些建構會透過改變狀態來控制。在函式語言中,迴圈和反覆運算會透過遞迴函式呼叫來取代/實作。許多此類語言保證在尾端位置進行的函式呼叫不會使用堆疊空間,因此遞迴迴圈會使用常數空間。由於 Clojure 使用 Java 呼叫慣例,因此它無法保證尾端呼叫最佳化,而且也不會保證。相反地,它提供 recur 特殊運算子,它會透過重新繫結和跳轉到最近的封閉迴圈或函式框架來進行常數空間遞迴迴圈。雖然它不像尾端呼叫最佳化那麼通用,但它允許使用大多數相同的優雅建構,並提供檢查 recur 呼叫只能在尾端位置發生的優點。

(defn my-zipmap [keys vals]
  (loop [my-map {}
         my-keys (seq keys)
         my-vals (seq vals)]
    (if (and my-keys my-vals)
      (recur (assoc my-map (first my-keys) (first my-vals))
             (next my-keys)
             (next my-vals))
      my-map)))
(my-zipmap [:a :b :c] [1 2 3])
-> {:b 2, :c 3, :a 1}

對於需要相互遞迴的情況,recur 無法使用。相反地,trampoline 可能是一個好選擇。