跳转到内容

学习 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*

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
分支和单子
特殊形式
华夏公益教科书