跳至内容

学习 Clojure/数据类型

来自维基教科书,开放世界中的开放书籍

关于 Clojure 的所有类型,有几点值得注意的事情

  1. Clojure 是用 Java 实现的:编译器是用 Java 编写的,Clojure 代码本身作为 Java VM 代码运行。因此,Clojure 中的数据类型是 Java 数据类型:Clojure 中的所有值都是普通的 Java 引用对象, Java 类的实例。
  2. 大多数 Clojure 类型是不可变的 一旦创建,它们永远不会改变。
  3. Clojure 更倾向于使用相等性比较而不是标识比较:例如,比较两个列表以查看它们是否是内存中的同一个对象,Clojure 的方式是比较它们实际的值, 它们的内容。大多数语言(包括 Java)不这样做,因为检查深度结构化对象的值代价高昂,但 Clojure 使其变得廉价:当创建一个 Clojure 对象时,它会保留一个自身的哈希值,在相等性比较中比较的是这个哈希值,而不是实际检查对象;只要比较的结构完全不可变,这个哈希值就足够了。(注意存储在不可变 Clojure 集合对象中的可变 Java 对象的情况。如果可变对象发生改变,这不会反映在集合的哈希值中。)

Java 为其原始数字类型包括包装引用类型,例如 java.lang.Integer "封装"(包装)原始 int 类型。因为每个 Clojure 函数都是一个 JVM 方法,它期望 Object 参数,所以 Java 原语通常在 Clojure 函数中被封装:当 Clojure 调用一个 Java 方法时,返回的原语会自动包装,并且传递给 Java 方法的任何参数会根据需要自动解包。(但是,类型提示允许 Clojure 函数中的非参数局部变量成为解包的原语,这在你尝试优化循环时很有用。)

Clojure 使用 java.lang.BigIntjava.lang.BigDecimal 类分别用于任意精度的整数和小数值。Clojure 算术运算的特殊版本(+'、-'、*'、inc' 和 dec')会根据需要智能地返回这些类型的值,以确保结果始终完全精确。

一些有理数 simply cannot be represented in floating-point, 所以 Clojure 添加了一个 Ratio 类型。Ratio 值是两个整数之间的比率。写成文字,Ratio 是两个整数之间用斜杠分隔,例如 23/55(二十三五十五分之二十三)。

Clojure 算术运算会根据需要智能地返回整数或比率,例如 7/3 加 2/3 返回 3,11 除以 5 返回 11/5。只要你的计算只涉及整数和比率,结果将是数学上完全准确的,但一旦浮点数或 BigDecimal 值进入混合,你将获得浮点数或 BigDecimal 结果,这可能会导致结果是数学上完全准确的,例如 1 除以 7 返回 1/7,但 1 除以 7.0 返回 0.14285714285714285。

字符串

[编辑 | 编辑源代码]

Clojure 中的字符串仅仅是 java.lang.String 的实例。与 Java 一样,字符串文字用双引号括起来,但与 Java 不同的是,字符串文字可以跨越多行。

java.lang.Character 文字写成 \ 后面跟着字符

\e
\t
\tab
\newline
\space

如你所见,空格字符写成 \ 后的单词。

布尔值

[编辑 | 编辑源代码]

文字 truefalse 分别表示值 java.lang.Boolean.TRUEjava.lang.Boolean.FALSE

在大多数 Lisp 方言中,有一个值与 Java null 类似,叫做 nil。在 Clojure 中,nil 就是 Java 的 null 值,故事到此结束。

在 Java 中,只有 truefalse 是条件表达式的合法值,但在 Clojure 中,条件表达式将 nil 视为具有false 真值。因此,虽然 !null(“非空”)是无效的 Java,但 Clojure 等价物 (not nil) 返回 true

Clojure 中的函数是一种对象类型,因此 Clojure 函数不仅可以被调用,还可以作为参数传递。由于 Clojure 是一种动态语言,Clojure 函数参数没有类型——任何类型的参数都可以传递给任何 Clojure 函数——但 Clojure 函数有一个设定的arity,因此如果你传递给函数错误的数量的参数,就会抛出异常。但是,函数的最后一个参数可以声明为接受任何额外的参数作为列表(就像 Java 中的“可变参数”一样),这样函数就可以接受n 个或更多参数。

Var 是 Clojure 中为数不多的可变类型之一。Var 基本上是一个用于保存另一个对象的存储单元——一个集合,基本上就是一组。

一个单独的 Var 实际上可以构成多个引用:一个根绑定(对所有线程可见的绑定)和任意数量的线程局部绑定(每个绑定对单个线程可见)。当访问 Var 的值时,访问的绑定可能取决于执行访问的线程:如果 Var 对该线程具有线程局部绑定,则返回 Var 的线程局部绑定的值;否则,返回 Var 的根绑定值(如果有)。

通常,Clojure 中的所有全局函数和变量都存储在 Var 的根绑定中。因为 Var 是可变的,所以我们可以更改 Var 的值来修改正在运行的系统。例如,我们可以用一个修复后的替换来替换一个有缺陷的函数。这是有效的,因为在 Clojure 中,编译后的函数绑定到保存它调用的函数的 Var,而不是 函数本身,也不是 用于指定 Var 的名称;由于 Var 是可变的,因此函数调用的函数可以在不重新定义函数的情况下发生更改。

Clojure 中的局部参数和变量是不可变的:它们在生命周期的开始被绑定,并且之后再也不会被绑定。然而,有时我们确实想要可变局部变量,而具有线程局部绑定的 Var 可以满足这一目的。

线程局部绑定还允许我们仅在本地上下文的范围内进行修改。假设我们有一个函数 cat,它调用存储在 Var 中的函数;如果一个函数 goat 被根绑定到 Var,那么 cat 通常会调用 goat;但是,如果我们在一个范围中调用 cat,在这个范围中,我们将一个函数 moose 线程局部绑定到该 Var,那么 cat 将调用 moose 而不是 goat

命名空间

[编辑 | 编辑源代码]

你应该将代码组织到命名空间中。Clojure 命名空间是一个表示符号值到 Var 和/或 java.lang.Class 对象的映射的对象。

  • Var 可以引用内联在命名空间中:区别在于 Var 只可以内联在一个命名空间中,但可以引用在任意数量的命名空间中。换句话说,Var 被内联的命名空间是它“真正”所属的命名空间。
  • 类只能引用,不能内联在命名空间中。当创建一个命名空间时,它会自动包含对 java.lang 的类进行引用。

从某种意义上说,命名空间本身存在于一个全局命名空间中:命名空间名称对单个命名空间是唯一的,例如 你永远不会拥有超过一个名为 foo 的命名空间。

当 Clojure 启动时,它会创建一个名为 clojure 的命名空间,在其中将符号 *ns* 映射到一个 Var,该 Var 用于保存“当前命名空间”。然后,Clojure 运行一个名为 core.clj 的脚本,它在 clojure 中内联许多标准函数,包括用于操作当前命名空间的函数,例如

  • in-ns 将当前命名空间设置为特定命名空间(直接操作 clojure/*ns* 是不被鼓励的)。
  • import 将 Class 对象引用到当前命名空间。
  • refer 将另一个命名空间的内联 Var 引用到当前命名空间。

在 Lisp 中,通常在其他语言中称为标识符的东西称为符号。然而,符号不仅仅是编译器看到的名称,而是一种值,一种类似字符串的值—— 一系列字符。由于符号是一种值,因此符号可以存储在集合中,作为参数传递给函数,等等,就像任何其他对象一样。

符号只能包含字母数字字符和 * + ! / . : - _ ?,但不能以数字或冒号开头

rubber-baby-buggy-bumper!      ; valid
j3_!:7                         ; valid
HELICOPTER                     ; valid 
+fiduciary+                    ; valid
3moose                         ; invalid
rubber baby buggy bumper       ; invalid

包含 / 的符号是命名空间限定

foo/bar    ; a symbol qualified with the namespace name "foo"

包含 . 的符号在评估时会得到特殊处理,我们将在后面看到。

Clojure 的一个关键特性是它的标准集合类型——主要是列表和哈希映射——都是持久的。持久集合是一个不可变的对象,但从现有集合中生成新的集合是廉价的,因为不需要复制现有数据。例如,将元素追加到持久列表的操作实际上不会修改列表,而是返回一个新的列表,该列表与原始列表相同,但多了一个元素;此新列表的创建成本很低,因为它主要只需要创建一个新节点并将其链接到已存在的列表节点,这些节点现在由两个列表共享。原始集合和新集合都具有相同的性能特征。

  • 列表

Clojure 持久列表类型是一个单向链表,用括号表示字面量

(53 "moo" asdf)   ; a list of three elements: a number, a string, and a symbol
  • 向量

单向链表在性能方面通常不合适,因此 Clojure 包含了一种名为向量的类型。Clojure 向量是一个有序的一维序列,类似于列表,但向量是使用类似哈希映射的结构实现的,因此索引查找时间为O(log32 n)而不是O(n)。向量用方括号表示字面量

[53 "moo" asdf]    ; a vector of three elements: a number, a string, and a symbol
  • 哈希映射

哈希映射用花括号表示字面量,使得每组两个参数都是一个键值对

{35 "moo" "quack" 21}   ; a hashmap with the key-value pairs 35 -> "moo" and "quack" -> 21
  • 序列

序列不是实际的集合类型,而是列表、向量、哈希映射以及所有其他 Clojure 集合类型都符合的接口。序列支持操作firstrestfirst检索集合的第一个项,而rest检索所有剩余项的序列。正如我们将看到的,序列支持大量建立在这两个基本操作之上的操作。

(当从映射生成序列时,first表示将映射中的单个对作为向量检索;返回的对在程序员看来实际上是随机的。映射序列的rest是所有剩余对作为向量的序列。)

关键字

[编辑 | 编辑源代码]

关键字是符号的一种变体,其特点是在前面加上冒号

:rubber-baby-buggy-bumper!      ; valid
:j3_!:7                         ; valid
:HELICOPTER                     ; valid 
:+fiduciary+                    ; valid

关键字的存在仅仅是因为,正如您将看到的,在代码中使用符号类似但实际上不是符号的名称很有用。默认情况下,关键字没有命名空间限定。但是,在某些情况下,生成一个有命名空间限定的关键字以避免与其他代码的命名冲突可能很有用。为此,可以显式地限定命名空间或键入由两个冒号前缀的符号

::gina     ; equivalent to :adam/gina (assuming this is in the namespace "adam")

;; in the REPL, after (in-ns 'adam) and (clojure.core/refer 'clojure.core)
adam=> (namespace :gina)        ; no namespace
nil
adam=> (namespace ::gina)
"adam" 
adam=> (namespace :adam/gina) 
"adam"

注意:程序生成的关键字与命名空间有关,存在一个注意事项。可以生成一个看起来属于命名空间的关键字,但(namespace)将返回nil

; use (namespace) to see what the namespace of the returned keywords is
user=> (keyword "test")        ; a keyword with no namespace
:test
user=> (keyword "user" "test") ; a keyword in the user namespace
:user/test
user=> (keyword "user/test")   ; a keyword that has no namespace but looks like it does!
:user/test

元数据

[编辑 | 编辑源代码]

元数据是描述其他数据的 data。Clojure 对象可以将一个其他对象(任何实现IPersistentMap的对象)作为元数据附加到它,例如向量可以将一个哈希映射作为元数据附加到它。

将元数据附加到对象不会修改对象,而是创建一个新对象——实际上,具有不同元数据的对象是不同的对象。但是,相等性比较会忽略元数据。


Previous page
基本操作
学习 Clojure Next page
数据结构
数据类型
华夏公益教科书