Clojure

特殊形式

(def symbol doc-string? init?)

建立並實習或找到具有symbol 名稱和當前命名空間值 (*ns*) 的命名空間的全局 var。如果提供 init,則會評估它,並且 var 的根繫結會設定為結果值。如果未提供 init,則 var 的根繫結不受影響。def 永遠套用於根繫結,即使在呼叫 def 的點上 var 是執行緒繫結的。def 產生 var 本身 (不是它的值)。如果 symbol 已在命名空間中且未對應到實習的 var,則會擲回例外。在 Clojure 1.3 中新增對 doc-string 的支援。

symbol 上的任何元資料都將會評估,並成為 var 本身的元資料。有幾個元資料鍵具有特殊解釋

  • :private

    布林值,表示 var 的存取控制。如果此鍵不存在,則預設存取權為公開 (例如,就像 :private false)。

  • :doc

    包含 var 內容的簡短 (1-3 行) 文件的字串

  • :test

    使用 assert 檢查各種運算的無參數函數。在評估元資料映射中的字面函數時,var 本身將會是可以存取的。

  • :tag

    命名類別或 Class 物件的符號,表示 var 中物件的 Java 類型,或如果物件是函數,則表示其回傳值。

此外,編譯器會在 var 上放置下列元資料鍵

  • :file 字串

  • :line 整數

  • :name 簡單符號

  • :ns 實習 var 的命名空間

  • :macro 如果 var 命名巨集,則為 true

  • :arglists 提供給 defn 的參數形式的向量清單

var 元資料也可以用於特定應用程式的目的。考慮使用命名空間限定鍵 (例如,:myns/foo) 以避免衝突。

(defn
 ^{:doc "mymax [xs+] gets the maximum value in xs using > "
   :test (fn []
             (assert (= 42  (mymax 2 42 5 4))))
   :user/comment "this is the best fn ever!"}
  mymax
  ([x] x)
  ([x y] (if (> x y) x y))
  ([x y & more]
   (reduce mymax (mymax x y) more)))

user=> (meta #'mymax)
  {:name mymax,
   :user/comment "this is the best fn ever!",
   :doc "mymax [xs+] gets the maximum value in xs using > ",
   :arglists ([x] [x y] [x y & more])
   :file "repl-1",
   :line 126,
   :ns #<Namespace user >,
   :test #<user$fn__289 user$fn__289@20f443 >}

許多巨集會擴充為 def (例如,defndefmacro),因此也會傳達使用作為名稱的 symbol 的結果 var 的元資料。

使用 def 來修改變數在頂層以外的根值通常表示你正在使用變數作為可變的全球變數,而且被認為是糟糕的風格。考慮使用繫結來提供變數的執行緒本地值,或在變數中放入 refagent,並使用交易或動作來進行變異。

(if 測試 如果是 否則?)

評估 測試。如果不是單數值 nilfalse,評估並產生 如果是,否則評估並產生 否則。如果未提供 否則,它預設為 nil。Clojure 中所有其他條件式都基於相同的邏輯,也就是說,nilfalse 構成邏輯假,其他所有內容構成邏輯真,而且這些含義適用於所有情況。if 對布林 Java 方法回傳值執行條件測試,而不會轉換為布林值。請注意,if 僅測試 java.lang.Boolean 的單數值,而不是任意值,僅測試單數值 false (Java 的 Boolean.FALSE),因此如果你正在建立自己的方框化布林值,請務必使用 Boolean/valueOf,而不是布林值建構函式。

(do expr*)

依序評估表達式 expr,並回傳最後一個的值。如果未提供任何表達式,則回傳 nil

(let [ 繫結* ] expr*)

繫結繫結形式 init-expr

在詞彙環境中評估表達式 expr,其中 繫結形式 中的符號繫結到它們各自的 init-expr 或其中的部分。繫結是順序的,因此每個 繫結 都可以看到先前的繫結。expr 包含在一個隱含的 do 中。如果 繫結 符號註解有元資料標籤,編譯器將嘗試將標籤解析為類別名稱,並假設在後續對 繫結 的參照中為該類型。最簡單的 繫結形式 是符號,它繫結到整個 init-expr

(let [x 1
      y x]
  y)
-> 1

有關繫結形式的更多資訊,請參閱 繫結形式

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

(quote 形式)

產生未評估的 形式

user=> '(a b c)
(a b c)

請注意,沒有嘗試呼叫函式 a。回傳值是 3 個符號的清單。

(var 符號)

符號必須解析為 var,而 Var 物件本身(非其值)會傳回。讀取巨集 #'x 會擴充為 (var x)

(fn 名稱? [參數* ] 表達式*)

(fn 名稱? ([參數* ] 表達式*)+)

參數位置參數*,或 位置參數* & 剩餘參數
位置參數繫結形式
剩餘參數繫結形式
名稱符號

定義函式 (fn)。Fn 是實作 IFn 介面 的一級物件。IFn 介面定義一個 invoke() 函式,其重載的 arity 範圍為 0-20。單一 fn 物件可以實作一個或多個 invoke 方法,因此可以對 arity 進行重載。只有一個重載本身可以是可變參數的,方法是指定連字符後接單一 剩餘參數。此類可變參數進入點在使用超過位置參數的引數呼叫時,會將它們收集到 seq 中,並繫結到剩餘參數或由剩餘參數解構。如果提供的引數沒有超過位置參數,剩餘參數將為 nil

第一個形式定義具有單一 invoke 方法的 fn。第二個形式定義具有多個重載 invoke 方法的 fn。重載的 arity 必須不同。在任一種情況下,表達式的結果都是單一 fn 物件。

表達式 expr 會在 參數繫結到實際引數的環境中編譯。expr 會封裝在隱含的 do 中。如果提供名稱 符號,它會在函式定義中繫結到函式物件本身,允許自呼叫,即使是在匿名函式中。如果 參數 符號加上元資料標記,編譯器會嘗試將標記解析為類別名稱,並在後續對繫結的參照中假設該類型。

(def mult
  (fn this
      ([] 1)
      ([x] x)
      ([x y] (* x y))
      ([x y & more]
          (apply this (this x y) more))))

請注意,例如 mult 等命名 fn 通常會使用 defn 定義,而 defn 會擴充為類似上述的內容。

一個 fn (重載) 定義一個遞迴點在函式的頂端,具有等於 param 數量的 arity,包括 rest param,如果存在。請參閱 recur

fns 實作 Java CallableRunnableComparator 介面。

自 1.1 起

函式支援指定執行時期的前置和後置條件。

函式定義的語法變成以下

(fn name? [param* ] condition-map? expr*)

(fn name? ([param* ] condition-map? expr*)+)

語法擴充也適用於 defn 和其他巨集,它們會擴充成 fn 形式。

注意:如果參數向量後面的唯一形式是一個映射,它會被視為函式主體,而不是條件映射。

condition-map 參數可以用來指定函式的條件和後置條件。它的形式如下

{:pre [pre-expr*]
 :post [post-expr*]}

其中任一鍵都是可選的。條件映射也可以提供為 arglist 的元資料。

pre-exprpost-expr 是布林表達式,可能參考函式的參數。此外,% 可以用在 post-expr 中來參考函式的回傳值。如果任何條件評估為 false*assert* 為 true,則會擲出 java.lang.AssertionError 例外。

範例

(defn constrained-sqr [x]
    {:pre  [(pos? x)]
     :post [(> % 16), (< % 225)]}
    (* x x))

有關繫結形式的更多資訊,請參閱 繫結形式

(loop [binding* ] expr*)

loop 正好和 let 一樣,除了它在迴圈頂端建立一個遞迴點,具有等於繫結數量的 arity。請參閱 recur

(recur expr*)

依序評估表達式 expr,然後並行地將遞迴點的繫結重新繫結到 expr 的值。如果遞迴點是 fn 方法,則會重新繫結參數。如果遞迴點是 loop,則會重新繫結 loop 繫結。然後執行會跳回遞迴點。recur 表達式必須與遞迴點的元數完全相符。特別是,如果遞迴點是變數 fn 方法的頂端,則不會收集 rest 參數 - 應該傳遞單一 seq(或 null)。recur 在尾端位置以外的位置會產生錯誤。

請注意,recur 是 Clojure 中唯一不消耗堆疊的迴圈建構。沒有尾端呼叫最佳化,不建議使用自我呼叫來迴圈未知邊界。recur 是函數式的,編譯器會驗證它在尾端位置的使用。

(def factorial
  (fn [n]
    (loop [cnt n acc 1]
       (if (zero? cnt)
            acc
          (recur (dec cnt) (* acc cnt))))))

(throw expr)

評估 expr 並擲出,因此它應該產生 Throwable 的某個衍生類別的實例。

(try expr* catch-clause* finally-clause?)

catch-clause → (catch classname name expr*)
finally-clause → (finally expr*)

評估 expr,如果沒有發生例外,則傳回最後一個表達式的值。如果發生例外且提供了 catch-clause,則會依序檢查每個 catch-clause,並考慮擲出的例外是 classname 的實例的第一個 catch-clause 為相符的 catch-clause。如果有相符的 catch-clause,則會在 name 繫結到擲出的例外的環境中評估其 expr,最後一個 expr 的值是函數的傳回值。如果沒有相符的 catch-clause,則例外會傳播到函數之外。在正常或異常傳回之前,任何 finally-clause expr 都會評估其副作用。

(monitor-enter expr)

(monitor-exit expr)

這些是同步原語,在使用者程式碼中應避免使用。請使用 locking 巨集。

其他特殊形式

特殊形式 點 ('.')newset! 的欄位說明在參考文件中的 Java 互操作 區段中。

set! 的變數說明在參考文件中的 變數 區段中。

繫結形式(解構)

Clojure 中最簡單的 binding-form 是符號。然而,Clojure 也支援抽象結構繫結,稱為 let 繫結清單、fn 參數清單中的解構,以及擴充至 letfn 的任何巨集。解構是一種使用類似集合作為繫結形式,建立一組繫結至集合中值的途徑。向量形式透過順序集合中的位置指定繫結,而映射形式則透過關聯集合中的鍵指定繫結。解構形式可以出現在任何 binding-form 出現的地方,因此可以巢狀,產生比使用集合存取器更清晰的程式碼。

由於資料不存在(例如順序結構中的元素太少、關聯結構中沒有鍵等),而與其各自部分不匹配的 binding-form 會繫結至 nil

順序解構

向量 binding_form 會依序繫結集合中的值,例如向量、清單、順序、字串、陣列,以及任何支援 nth 的項目。順序解構形式是 binding-form 的向量,會繫結至 init-expr 中的連續元素,透過 nth 查詢。此外,也可以選擇在 & 之後繫結 binding-form 至順序的剩餘部分,也就是尚未繫結的部分,並透過 nthnext 查詢。

最後,也可以選擇在符號之後加上 :as,將該符號繫結至整個 init-expr

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])

->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

這些形式可以巢狀

(let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]
  [x1 y1 x2 y2])

->[1 2 3 4]

在所有順序案例中,解構繫結中的 binding-form 會與目標資料結構中所需值所在的位址相符。

關聯解構

映射 binding-form 會透過查詢映射、集合、向量、字串和陣列(後三者具有整數鍵)中的值來建立繫結。它包含 binding-form→key 成對的映射,每個 binding-form 會繫結至 init-expr 中,在提供的鍵中的值。此外,也可以選擇在繫結形式中,在符號之後加上 :as,將該符號繫結至整個 init-expr。此外,也可以選擇在繫結形式中,在另一個映射之後加上 :or 鍵,用於提供一些或所有鍵的預設值,如果這些鍵未在 init-expr 中找到

(let [{a :a, b :b, c :c, :as m :or {a 2 b 3}}  {:a 5 :c 6}]
  [a b c m])

->[5 3 6 {:c 6, :a 5}]

通常會希望使用與對應映射鍵相同名稱的符號來繫結。:keys 指令可解決 binding-form→key 成對繫結中常見的冗餘

(let [{fred :fred ethel :ethel lucy :lucy} m] ...

可以寫成

(let [{:keys [fred ethel lucy]} m] ...

從 Clojure 1.6 開始,您也可以在 map 解構形式中使用前綴 map 鍵

(let [m {:x/a 1, :y/b 2}
      {:keys [x/a y/b]} m]
  (+ a b))

-> 3

在使用前綴鍵的情況下,繫結符號名稱與前綴鍵的右側相同。您也可以在 :keys 指令中使用自動解析的關鍵字形式

(let [m {::x 42}
      {:keys [::x]} m]
  x)

-> 42

有類似的 :strs:syms 指令用於匹配字串和符號鍵,後者自 Clojure 1.6 開始也允許前綴符號鍵。

Clojure 1.9 新增支援,可以使用下列解構鍵形式直接解構共用相同命名空間的許多鍵(或符號)

  • :ns/keys - ns 指定要從輸入中查詢的鍵的預設命名空間

    • 鍵元素不應指定命名空間

    • 鍵元素也定義新的區域符號,就像 :keys 一樣

  • :ns/syms - ns 指定要從輸入中查詢的符號的預設命名空間

    • 符號元素不應指定命名空間

    • 符號元素也定義新的區域符號,就像 :syms 一樣

(let [m #:domain{:a 1, :b 2}
      {:domain/keys [a b]} m]
  [a b])

-> [1 2]

關鍵字引數

關鍵字引數是 akey aval bkey bval…​ 形式的選用尾隨變數引數,可以在函式主體中透過關聯解構來存取。此外,Clojure 1.11 中引入的函式指定為接受關鍵字引數,可以傳遞單一 map,而不是或除了(和之後的)鍵/值對。傳遞單一 map 時,會直接用於解構,否則會透過 conj 將尾隨 map 加入從前一個鍵/值建立的 map。若要定義接受關鍵字引數的函式,您可以在 rest-param 宣告位置提供 map 解構形式。例如,定義一個函式,它會取得序列和選用的關鍵字引數,並傳回包含值的向量,如下所示

(defn destr [& {:keys [a b] :as opts}]
  [a b opts])

(destr :a 1)
->[1 nil {:a 1}]

(destr {:a 1 :b 2})
->[1 2 {:a 1 :b 2}]

destr& 右側的 map binding-form 是關聯解構 binding-form詳情如上所述

以下兩個 foo 宣告是等價的,展示了關聯解構對 seq 的詮釋

(defn foo [& {:keys [quux]}] ...)

(defn foo [& opts]
  (let [{:keys [quux]} opts] ...))

巢狀解構

由於繫結形式可以任意巢狀在彼此之中,你可以拆解幾乎任何東西

(let [m {:j 15 :k 16 :ivec [22 23 24 25]}
      {j :j, k :k, i :i, [r s & t :as v] :ivec, :or {i 12 j 13}} m]
  [i j k r s t v])

-> [12 15 16 22 23 (24 25) [22 23 24 25]]