Clojure

特殊形式

特殊形式具有与标准 Clojure 求值规则不同的求值规则,并由 Clojure 编译器直接理解。

特殊形式的标题使用正则表达式语法非正式地描述了特殊形式的语法:? (可选),* (0 个或多个) 和 + (1 个或多个)。非终结符用斜体表示。

(def symbol doc-string? init?)

创建和插入或定位一个全局变量,其名称为symbol,命名空间为当前命名空间 (*ns*) 的值。如果提供了init,则对其进行求值,并将变量的根绑定设置为结果值。如果未提供init,则变量的根绑定不受影响。即使在调用def的地方变量是线程绑定的,def也始终应用于根绑定。def返回变量本身(而不是其值)。如果symbol已存在于命名空间中且未映射到已插入的变量,则抛出异常。对doc-string 的支持是在 Clojure 1.3 中添加的。

symbol 上的任何元数据都将被求值,并成为变量本身的元数据。有几个元数据键具有特殊的解释

  • :private

    一个布尔值,指示变量的访问控制。如果此键不存在,则默认访问权限为公共(例如,如果:private false)。

  • :doc

    一个包含变量内容的简短(1-3 行)文档的字符串

  • :test

    一个无参数的 fn,它使用assert来检查各种操作。在求值元数据映射中的字面 fn 时,变量本身将是可访问的。

  • :tag

    一个命名类或 Class 对象的符号,指示变量中对象的 Java 类型,或者如果对象是 fn,则指示其返回值。

此外,编译器将在变量上放置以下元数据键

  • :file 字符串

  • :line 整数

  • :name 简单符号

  • :ns 变量被插入的命名空间

  • :macro true 如果变量命名一个宏

  • :arglists 参数形式的向量列表,如提供给defn的一样

变量元数据也可以用于应用程序特定的目的。考虑使用命名空间限定的键(例如: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 传达了结果变量的元数据。

除了在顶层以外,使用def修改变量的根值通常表示您正在将变量用作可变全局变量,并且被认为是糟糕的风格。考虑要么使用绑定为变量提供线程局部值,要么将引用代理放入变量中,并使用事务或操作进行修改。

(if test then else?)

求值test。如果它不是单一的值nilfalse,则求值并返回then,否则求值并返回else。如果未提供else,则默认为nil。Clojure 中所有其他条件语句都基于相同的逻辑,即nilfalse构成逻辑假,而其他所有内容构成逻辑真,并且这些含义在整个过程中都适用。if对布尔 Java 方法返回值进行条件测试,无需转换为 Boolean。请注意,if不会测试 java.lang.Boolean 的任意值,只会测试单一值false(Java 的Boolean.FALSE),因此,如果您正在创建自己的装箱布尔值,请确保使用Boolean/valueOf,而不是 Boolean 构造函数。

(do expr*)

按顺序求值表达式exprs,并返回最后一个表达式的值。如果未提供任何表达式,则返回nil

(let [ binding* ] expr*)

bindingbinding-form init-expr

在词法上下文中求值表达式exprs,其中binding-forms 中的符号绑定到它们各自的init-exprs 或其部分。绑定是顺序的,因此每个binding都可以看到前面的绑定。exprs 包含在一个隐式的do中。如果binding 符号用元数据标签进行注释,则编译器将尝试将标签解析为类名,并在后续对binding 的引用中假定该类型。最简单的binding-form 是一个符号,它绑定到整个init-expr

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

有关绑定形式的更多信息,请参见绑定形式

使用let创建的局部变量不是变量。一旦创建,它们的值永远不会改变!

(quote form)

返回未求值的form

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

请注意,没有尝试调用函数a。返回值是包含 3 个符号的列表。

(var symbol)

symbol 必须解析为变量,并且返回变量对象本身(而不是其值)。读取器宏#'x扩展为(var x)

(fn name? [params* ] expr*)

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

paramspositional-param*,或positional-param* & rest-param
positional-parambinding-form
rest-parambinding-form
namesymbol

定义一个函数 (fn)。Fn 是实现IFn 接口 的一等公民。IFn 接口定义了一个invoke() 函数,该函数被重载,其元数范围从 0 到 20。单个 fn 对象可以实现一个或多个 invoke 方法,因此可以根据元数进行重载。只有一个重载可以是可变参数的,方法是指定与号后跟一个rest-param。当使用超过位置参数的参数调用此可变参数入口点时,它将在一个 seq 中收集这些参数,该 seq 绑定到 rest 参数,或被 rest 参数解构。如果提供的参数不超过位置参数,则 rest 参数将为nil

第一种形式定义了一个具有单个 invoke 方法的 fn。第二种形式定义了一个具有一个或多个重载的 invoke 方法的 fn。重载的元数必须是不同的。在这两种情况下,表达式的结果都是单个 fn 对象。

表达式exprs 在params 绑定到实际参数的环境中进行编译。exprs 被包含在一个隐式的do中。如果提供了名称symbol,它将在函数定义内绑定到函数对象本身,允许自调用,即使在匿名函数中也是如此。如果param 符号用元数据标签进行注释,则编译器将尝试将标签解析为类名,并在后续对绑定的引用中假定该类型。

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

请注意,命名 fn(例如mult)通常使用defn定义,它扩展为类似于上面的内容。

fn(重载)在函数的顶部定义一个递归点,其元数等于params 的数量包括 rest 参数(如果存在)。参见recur.

fn 实现 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*]}

其中任一键都是可选的。条件映射也可以作为参数列表的元数据提供。

pre-exprpost-expr 是布尔表达式,可以引用函数的参数。此外,% 可以用在post-expr 中来引用函数的返回值。如果任何条件求值为false*assert* 为真,则会抛出一个java.lang.AssertionError 异常。

示例

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

有关绑定形式的更多信息,请参见绑定形式

(loop [binding* ] expr*)

looplet完全相同,只是它在循环的顶部建立了一个递归点,其元数等于绑定的数量。参见recur.

(recur expr*)

按顺序评估表达式 exprs,然后并行地将递归点的绑定重新绑定到 exprs 的值。如果递归点是一个 fn 方法,则它会重新绑定参数。如果递归点是一个 loop,则它会重新绑定 loop 绑定。然后执行跳转回递归点。recur 表达式必须与递归点的参数个数完全匹配。特别是,如果递归点是可变参数 fn 方法的顶部,则不会收集 rest 参数 - 应该传递一个序列(或 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*)

exprs 会被评估,如果没有任何异常发生,则返回最后一个表达式的值。如果发生异常并且提供了 catch-clauses,则会依次检查每个 catch-clause,第一个与抛出的异常的类型相匹配的 catch-clause 将被认为是匹配的 catch-clause。如果有匹配的 catch-clause,则会在 name 绑定到抛出的异常的上下文中评估其 exprs,并且最后一个表达式的值是函数的返回值。如果没有匹配的 catch-clause,则异常会传播到函数之外。在正常或异常返回之前,任何 finally-clause exprs 都将被评估,以执行其副作用。

(monitor-enter expr)

(monitor-exit expr)

这些是同步原语,应该在用户代码中避免使用。请使用 locking 宏。

其他特殊形式

特殊形式 点 ('.')newset! 字段在 Java 交互操作 部分中描述。

set! vars 在 Vars 部分中描述。

绑定形式(解构)

Clojure 中最简单的 binding-form 是一个符号。但是,Clojure 也支持在 let 绑定列表、fn 参数列表中以及扩展到 letfn 的任何宏中进行抽象结构绑定,称为解构。解构是一种通过使用类似的集合作为绑定形式来创建对集合中值的绑定集的方法。矢量形式通过位置在顺序集合中指定绑定,映射形式通过键在关联集合中指定绑定。解构形式可以出现在任何 binding-form 可以出现的地方,因此可以嵌套,从而生成比使用集合访问器更清晰的代码。

由于数据缺失(例如,顺序结构中元素太少、关联结构中没有键等),与各自部分不匹配的 Binding-forms 将绑定到 nil

顺序解构

矢量 binding_forms 按顺序绑定集合中值的绑定,例如矢量、列表、seqs、字符串、数组以及支持 nth 的任何内容。顺序解构形式是 binding-forms 的一个矢量,它们将绑定到来自 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-forms 将与目标数据结构中所需值所在的位置匹配。

关联解构

映射 binding-forms 通过在集合中查找值来创建绑定,例如映射、集合、矢量、字符串和数组(后三个具有整数键)。它包含一个 binding-form→key 对的映射,每个 binding-form 绑定到 init-expr 中提供的键处的 value。此外,还可以选择,在绑定形式中,: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 开始,您还可以在映射解构形式中使用带前缀的映射键

(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 元素不应指定命名空间

    • syms 元素也定义新的局部符号,与 :syms 相同

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

-> [1 2]

关键字参数

关键字参数是形式为 akey aval bkey bval…​ 的可选尾部可变参数,可以通过关联解构在函数体中访问。此外,在 Clojure 1.11 中引入的,指定为接受 kwargs 的函数可以被传递一个单独的映射,而不是或除了(以及紧随其后)键/值对之外。当传递单个映射时,它将直接用于解构,否则将通过 conj 将尾部映射添加到从前面键/值对构建的映射中。要定义接受关键字参数的函数,您需要在 rest-param 声明位置提供一个映射解构形式。例如,一个接受序列和可选关键字参数并返回包含值的矢量的函数定义如下

(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& 右边的映射 binding-form 是一个关联解构 binding-form上面有详细介绍

下面两个 foo 的声明是等价的,展示了关联解构对 seqs 的解释

(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]]