學習 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*)
loop 與 let 相似,只是它為 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,將在下一節中介紹。)