42 ; integer
-1.5 ; floating point
22/7 ; ratio
以下是 Clojure 中常用基本类型的字面量表示示例。所有这些字面量都是有效的 Clojure 表达式。
;
创建一个注释到行尾。有时使用多个分号来表示标题注释部分,但这只是一个约定。
42 ; integer
-1.5 ; floating point
22/7 ; ratio
整数在范围内时被读取为固定精度 64 位整数,否则为任意精度。尾随的 N
可用于强制任意精度。Clojure 还支持 Java 语法用于八进制(前缀 0
)、十六进制(前缀 0x
)和任意基数(前缀基数,然后是 r
,例如 2r
用于二进制)整数。比率以其自己的类型提供,组合了分子和分母。
浮点数被读取为双精度 64 位浮点数,或使用 M
后缀进行任意精度。还支持指数表示法。特殊符号值 ##Inf
、##-Inf
和 ##NaN
分别表示正无穷大、负无穷大和“非数字”值。
"hello" ; string
\e ; character
#"[0-9]+" ; regular expression
字符串包含在双引号中,可以跨越多行。单个字符用前导反斜杠表示。有一些特殊的命名字符:\newline
\space
\tab
等。Unicode 字符可以使用 \uNNNN
表示,也可以使用八进制表示法 \oNNN
表示。
字面量正则表达式是前导为 #
的字符串。这些被编译为 java.util.regex.Pattern 对象。
map ; symbol
+ ; symbol - most punctuation allowed
clojure.core/+ ; namespaced symbol
nil ; null value
true false ; booleans
:alpha ; keyword
:release/alpha ; keyword with namespace
符号由字母、数字和其他标点符号组成,用于引用其他内容,如函数、值、命名空间等。符号可以选择性地包含命名空间,用正斜杠将其与名称分隔开。
有三个特殊的符号被读取为不同的类型 - nil
是空值,true
和 false
是布尔值。
关键字以冒号开头,始终求值为自身。它们在 Clojure 中经常用作枚举值或属性名称。
接下来我们将考虑 Clojure 如何读取和求值表达式。
在 Clojure 中,源代码被 读取器 作为字符读取。读取器可以从 .clj 文件读取源代码,也可以在交互方式下获得一系列表达式。读取器生成 Clojure 数据。然后,Clojure 编译器生成 JVM 的字节码。
这里有两个要点
源代码的单位是 **Clojure 表达式**,而不是 Clojure 源文件。源文件被读取为一系列表达式,就像你在 REPL 中交互式地输入这些表达式一样。
将读取器和编译器分开是一个关键的分离,它为宏腾出了空间。宏是特殊的函数,它们以代码(作为数据)作为输入,并以代码(作为数据)作为输出。你能看到在求值模型中可以插入宏展开循环的位置吗?
考虑一个 Clojure 表达式
此图说明了语法(绿色,由读取器生成的 Clojure 数据结构)和语义(蓝色,Clojure 运行时如何理解这些数据)之间的区别。
大多数字面量 Clojure 表达式求值为自身,**除了** 符号和列表。符号用于引用其他内容,并在求值时返回它们所引用的内容。列表(如图所示)被求值为调用。
在图中,(+ 3 4) 被读取为一个列表,该列表包含符号 (+) 和两个数字(3 和 4)。第一个元素(包含 + 的位置)可以称为“函数位置”,即查找要调用的内容的位置。虽然函数是显而易见的调用对象,但运行时还知道一些特殊的运算符、宏和一些其他可调用对象。
考虑上述表达式的求值
3 和 4 求值为自身(long 型)
+ 求值为一个实现 +
的函数
求值列表将调用 +
函数,并以 3 和 4 作为参数
许多语言都有语句和表达式,语句有一些有状态的效果,但不返回值。在 Clojure 中,一切都是一个表达式,求值为一个值。一些表达式(但不是大多数)也有副作用。
现在让我们考虑如何在 Clojure 中交互式地求值表达式。
有时延迟求值很有用,特别是对于符号和列表。有时符号应该只是一个符号,而不查找它所引用的内容
user=> 'x
x
有时列表应该只是一组数据值(而不是要求值的代码)
user=> '(1 2 3)
(1 2 3)
你可能会看到的一个令人困惑的错误是,意外地尝试将一组数据作为代码求值
user=> (1 2 3)
Execution error (ClassCastException) at user/eval156 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn
现在,不要太担心引用,但你会在这些材料中偶尔看到它,以避免符号或列表的求值。
在使用 Clojure 的大多数情况下,你会在编辑器或 REPL(Read-Eval-Print-Loop,读-求值-打印-循环)中这样做。REPL 包含以下部分
读取一个表达式(一串字符)以生成 Clojure 数据。
求值从 #1 返回的数据,以生成结果(也是 Clojure 数据)。
通过将结果从数据转换回字符来打印结果。
循环回到开头。
#2 的一个重要方面是,Clojure 始终在执行之前编译表达式;Clojure **始终** 被编译为 JVM 字节码。没有 Clojure 解释器。
user=> (+ 3 4)
7
上面的框演示了求值表达式 (+ 3 4) 并接收结果。
大多数 REPL 环境支持一些技巧,以帮助交互式使用。例如,一些特殊符号记住最后三个表达式的结果
*1
(最后一个结果)
*2
(两个表达式之前的结果)
*3
(三个表达式之前的结果)
user=> (+ 3 4)
7
user=> (+ 10 *1)
17
user=> (+ *1 *2)
24
此外,有一个命名空间 clojure.repl
包含在标准 Clojure 库中,它提供了一些有用的函数。要加载该库并使其函数在当前上下文中可用,请调用
(require '[clojure.repl :refer :all])
现在,你可以将其视为一个神奇的咒语。砰!当我们谈到命名空间时,我们将解释它。
我们现在可以使用一些在 REPL 中有用的额外函数:doc
、find-doc
、apropos
、source
和 dir
。
doc
函数显示任何函数的文档。让我们在 +
上调用它
user=> (doc +)
clojure.core/+
([] [x] [x y] [x y & more])
Returns the sum of nums. (+) returns 0. Does not auto-promote
longs, will throw on overflow. See also: +'
doc
函数打印 +
的文档,包括有效的签名。
doc 函数打印文档,然后返回 nil 作为结果 - 你将在求值输出中看到两者。
我们也可以在自身上调用 doc
user=> (doc doc)
clojure.repl/doc
([name])
Macro
Prints documentation for a var or special form given its name
不确定某件事叫什么?你可以使用 apropos
命令查找与特定字符串或正则表达式匹配的函数。
user=> (apropos "+")
(clojure.core/+ clojure.core/+')
你还可以将搜索范围扩大到包括 docstrings 本身,使用 find-doc
user=> (find-doc "trim")
clojure.core/subvec
([v start] [v start end])
Returns a persistent vector of the items in vector from
start (inclusive) to end (exclusive). If end is not supplied,
defaults to (count vector). This operation is O(1) and very fast, as
the resulting vector shares structure with the original and no
trimming is done.
clojure.string/trim
([s])
Removes whitespace from both ends of string.
clojure.string/trim-newline
([s])
Removes all trailing newline \n or return \r characters from
string. Similar to Perl's chomp.
clojure.string/triml
([s])
Removes whitespace from the left side of string.
clojure.string/trimr
([s])
Removes whitespace from the right side of string.
如果你想查看特定命名空间中的函数的完整列表,可以使用 dir
函数。这里,我们可以在 clojure.repl
命名空间上使用它
user=> (dir clojure.repl)
apropos
demunge
dir
dir-fn
doc
find-doc
pst
root-cause
set-break-handler!
source
source-fn
stack-element-str
thread-stopper
最后,我们可以不仅看到文档,还可以看到运行时可以访问的任何函数的底层源代码
user=> (source dir)
(defmacro dir
"Prints a sorted directory of public vars in a namespace"
[nsname]
`(doseq [v# (dir-fn '~nsname)]
(println v#)))
在学习本工作坊的过程中,请随意检查你正在使用的函数的 docstring 和源代码。探索 Clojure 库本身的实现是学习更多关于语言及其使用方式的绝佳途径。
在学习 Clojure 时,始终打开一份 Clojure 速查表 的副本也是一个好主意。速查表对标准库中可用的函数进行了分类,是一个宝贵的参考。
现在让我们考虑一些 Clojure 基础知识,让你开始...。
def
在 REPL 中求值时,保存一些数据以便以后使用会很有用。我们可以使用 def
来实现
user=> (def x 7)
#'user/x
def
是一个特殊的形式,它将当前命名空间中的符号(x)与一个值(7)关联起来。这种关联称为 var
。在大多数实际的 Clojure 代码中,var 应该引用常量值或函数,但在 REPL 中工作时,通常为了方便起见定义和重新定义它们。
注意上面的返回值是 #'user/x
- 这是 var 的字面量表示:#'
后跟命名空间符号。user
是默认命名空间。
回想一下,符号通过查找它们所引用的内容来求值,因此我们可以通过仅使用符号来获得回值
user=> (+ x x)
14
学习一门语言时最常见的事情之一就是打印出值。Clojure 提供了几个函数用于打印值
面向人类 | 可读为数据 | ||
---|---|---|---|
带换行符 |
println |
prn |
|
不带换行符 |
pr |
面向人类的格式将特殊打印字符(如换行符和制表符)转换为其打印形式,并在字符串中省略引号。我们通常使用 println
来调试函数或在 REPL 中打印值。println
接受任意数量的参数,并在每个参数的打印值之间插入一个空格
user=> (println "What is this:" (+ 1 2))
What is this: 3
println 函数有副作用(打印),并返回 nil 作为结果。
请注意,上面的“这是:”没有打印周围的引号,并且不是读取器可以再次读取为数据的字符串。
为此目的,使用 prn 作为数据打印
user=> (prn "one\n\ttwo")
"one\n\ttwo"
现在打印的结果是一个有效的形式,读取器可以再次读取。根据上下文,你可能更喜欢人类形式或数据形式。