学习 Clojure/特殊形式
Clojure 特殊形式的官方文档:https://clojure.net.cn/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
,将在下一节中介绍。)