跳轉到內容

學習 Clojure/特殊形式

來自華夏公益教科書,開放的書,開放的世界

Clojure 特殊形式的官方文件:http://clojure.org/special_forms#Special%20Forms

在任何 Lisp 方言中,您都需要特殊的“原語”,稱為特殊形式,它們以特殊方式進行評估。例如,Clojure 評估規則缺少一種進行條件評估的方法,因此我們有特殊形式 if

特殊形式引數的評估方式因每個特殊形式而異,這些評估可能會根據上下文而改變,例如特殊形式A 的評估可能會取決於它在特殊形式B 中的使用。Clojure 特殊形式是

(形式的引數用斜體表示。以 ? 結尾的引數是可選的。以 * 結尾的引數表示 0 個或多個引數。以 + 結尾的引數表示 1 個或多個引數。)


  • (if test then else?

如果test 返回真值(任何非假值或空值),則評估then 並返回。否則,評估可選的else? 並返回,如果未指定else?,則返回 nil

(if moose a b)        ; if moose is not false or nil, return a; otherwise, return b
(if (frog) (cow))     ; if (frog) returns something other than false or nil, return value of (cow); otherwise, return nil


  • (quote form

form 未經評估返回

(quote (foo ack bar))    ; returns the list (foo ack bar)
(quote bat)              ; returns the symbol bat itself, not the Var or value resolved from the symbol bat

當您希望將符號作為引數傳遞給常規函式時,這很有用

(foo (quote bar))        ; call function foo with argument symbol bar (if foo is a macro, the macro is passed the list (quote bar))
  • (var symbol

通常,解析為 Var 的符號會進一步評估為 Var 的值。特殊形式 var 從解析的符號中返回 Var 本身,而不是 Var 的值

(var goose)              ; return the Var mapped to goose

如果符號無法解析為 Var,則會丟擲異常。


  • (def symbol value

在當前名稱空間中,symbol 對映到包含value 的一個內部 Var。如果對映到該符號的 Var 已經存在,則 def 將為其分配新值。

(def george  7)      ; create/set a Var mapped to symbol george in the current namespace and give that Var the value 7 
(def george -3)      ; change the value of that Var to -3

def 返回受影響的 Var。

您可以 def 一個名稱空間限定的符號,但前提是該名稱空間中已存在一個以該名稱命名的 Var。

 (def nigeria/fred "hello")  ; change value of Var mapped to nigeria/fred
                             ; throws an exception if the Var mapped to nigeria/fred does not already exist

嘗試 def 到一個已經對映到引用 Var 的符號會丟擲異常。


  • (fn name? [params*] body*

返回一個新定義的函式物件。

name?: 函式內部看到的函式名稱;對於遞迴呼叫很有用。

params*: 繫結到區域性引數的符號。

body*: 當呼叫函式時要“評估”的引數;函式呼叫返回其主體中返回的最後一個值。

例如

(fn [] 3)                 ; returns a function which takes no arguments and returns 3
(fn [a b] (+ a b))        ; returns a function which returns the sum of its two arguments
(fn victor [] (victor))   ; returns a function which does nothing but infinitely recursively call itself

通常,由 fn 返回的函式物件以某種方式保留,要麼傳遞給函式,要麼繫結到 Var 或其他類似的變數。原則上,您可以立即呼叫返回的函式(雖然這不是明智的做法)

 ((fn [a b] (+ a b)) 3 5)   ; calls the function with args 3 and 5, returning 8


name?params* 提供的符號不會被解析,而是為函式主體建立區域性名稱。Clojure 是詞法作用域的,因此函式中區域性變數的繫結優先於函式外部的繫結,例如函式主體中的符號foo 將解析為當前名稱空間中的foo,僅當沒有區域性foo 並且沒有包含的函式具有名為foo 的區域性變數時。

當呼叫 Clojure 函式時,它的主體不會像您想象的那樣透過評估執行,實際上,fn 主體在返回之前會進行部分評估:符號被解析,宏被評估,這樣每次呼叫函式時都不會進行這些工作;此外,主體中的特殊形式會盡可能進行邏輯評估,例如 fn 被評估以避免以後進行工作,但 if 不會被評估,因為它代表著真正的工作,在實際呼叫函式之前沒有意義。此外,當呼叫函式時,Clojure 評估器實際上並不參與,因為 Clojure 將函式編譯成 JVM 位元組碼。考慮一下

(fn [] (frog 5))

當評估此特殊形式時,符號frog 會解析為當前名稱空間中的 Var。如果此 Var 在評估時包含宏,則擴充套件宏呼叫,否則將列表編譯成函式呼叫。假設frog 不是宏,那麼當我們定義的函式被呼叫並執行 (frog 5) 時,frog 的 Var 持有的函式將用引數 5 呼叫;如果 Var 在那時沒有引用函式,則會丟擲異常。

(實際上,當評估器遇到任何函式呼叫時——無論是在函式主體內部還是外部——它總是將其編譯成位元組碼;區別在於函式定義外部的呼叫在編譯後立即由評估器執行。)

通常,函式具有固定的元數它接受固定數量的引數。但是,函式的最後一個引數可以以 & 開頭,表示它以列表的形式接受 0 個或多個額外的引數

(fn [a b & c] ...)   ; takes 2 or more arguments; all arguments beyond 2 are passed as a list to c
(fn [& x] ...)       ; takes 0 or more arguments; all arguments passed as a list to x

(通常,& 只是一個像其他符號一樣的符號,但它在 fn 中為此目的進行了特殊處理,因此實際上,您不能擁有名為 & 的區域性變數。)

可以使用此形式為不同元數定義具有不同主體的一個函式

(fn name? ([params*] body*)+)

例如

(fn ([] 1)              ; a function which can be called with 0, 1, 2, or 3-or-more arguments
    ([a] 2)             ; returns a different number based on how many args are passed to it
    ([a b] 3)
    ([a b c & d] 4))

在這種函式中,只有一個主體可以具有可變元數,並且該主體的元數必須大於任何其他主體的元數。


  • (do body*

body 是任何數量的引數,按順序進行評估;最後一個引數的值將被返回。(我們通常不經常使用 do,因為函式主體實際上是一個隱式 do,它通常滿足我們的需求。)


  • (let [local*] body*

其中 local => name value

宣告一個區域性範圍,其中存在一個或多個區域性變數

; a local scope in which aaron is bound to the value 3 
; while bill is bound to the value returned by (moose true)
(let [aaron 3 
      bill (moose true)]
   (print aaron)
   (print bill))

定義的區域性變數是不可變的,並且僅在 let 內部可見。

區域性名稱可以用於在列表中定義另一個區域性變數

(let [mike 6
      kim mike]  ; local kim defined by value of mike
 ;...
)


  • (recur args*

recur 將執行傳送回最後一個“遞迴點”,這通常是緊鄰的函式。與常規遞迴呼叫不同,recur 會重複使用當前的堆疊幀,因此它實際上是 Clojure 進行尾遞迴的方式。recur 必須在“尾部位置”使用(作為函式中可能評估的最後一個表示式)

(defn factorial [n]
  (defn fac [n acc]
    (if (zero? n)
       acc
      (recur (- n 1) (* acc n)))) ; recursive call to fac, but reuses the stack; n will be (- n 1), and acc will be (* acc n)
  (fac n 1))

將來,JVM 可能會新增對尾呼叫最佳化的內建支援,屆時 recur 將變得不再必要。


  • (loop [params*] body*

looplet 相似,只是它為 recur 建立了一個“遞迴點”。實際上,loop 是在函式中進行迭代的基本方法

(def factorial
  (fn [n]
    (loop [cnt n acc 1]
      (if (zero? cnt)
         acc
        (recur (dec cnt) (* acc cnt))))))  ; send execution back to the enclosing loop with new bindings
                                           ; cnt will be (dec cnt) and acc will be (* acc cnt)
  • (throw expr

相當於 Java 中的 throw

(throw (rat))        ; throw the exception returned by (rat)
(throw newt)         ; throw the exception named by newt

就像在 Java 中一樣,丟擲的物件必須是 java.lang.Throwable 型別或其子類。


  • (try body* (catch class name body*)* (finally body*)?

相當於 Java 中的 try-catch-finally

(try
  (bla)
  (bla)
  (catch Antelope x
    (bla x))
  (catch Gorilla y
    (bla y)
  (finally
    (bla)))
  • (monitor-enter)
  • (monitor-exit)

Hickey 說,“這些是同步原語,應該在使用者程式碼中避免”。您應該改為使用宏 clojure/locking


  • (set!)


(另外兩個特殊形式,.new,將在下一節中介紹。)

Previous page
元資料
學習 Clojure Next page
分支和單子
特殊形式
華夏公益教科書