Clojure

学习 Clojure - 语法

字面量

以下是 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 是空值,truefalse 是布尔值。

关键字以冒号开头,始终求值为自身。它们在 Clojure 中经常用作枚举值或属性名称。

字面量集合

Clojure 还包含四种集合类型的字面量语法

'(1 2 3)     ; list
[1 2 3]      ; vector
#{1 2 3}     ; set
{:a 1, :b 2} ; map

我们将在后面详细讨论这些内容 - 现在,知道这四种数据结构可以用来创建复合数据就足够了。

求值

接下来我们将考虑 Clojure 如何读取和求值表达式。

传统求值 (Java)

Java evaluation

在 Java 中,源代码(.java 文件)被编译器(javac)作为字符读取,编译器生成字节码(.class 文件),这些字节码可以被 JVM 加载。

Clojure 求值

Clojure evaluation

在 Clojure 中,源代码被 读取器 作为字符读取。读取器可以从 .clj 文件读取源代码,也可以在交互方式下获得一系列表达式。读取器生成 Clojure 数据。然后,Clojure 编译器生成 JVM 的字节码。

这里有两个要点

  1. 源代码的单位是 **Clojure 表达式**,而不是 Clojure 源文件。源文件被读取为一系列表达式,就像你在 REPL 中交互式地输入这些表达式一样。

  2. 将读取器和编译器分开是一个关键的分离,它为宏腾出了空间。宏是特殊的函数,它们以代码(作为数据)作为输入,并以代码(作为数据)作为输出。你能看到在求值模型中可以插入宏展开循环的位置吗?

结构 vs 语义

考虑一个 Clojure 表达式

Structure and semantics

此图说明了语法(绿色,由读取器生成的 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

现在,不要太担心引用,但你会在这些材料中偶尔看到它,以避免符号或列表的求值。

REPL

在使用 Clojure 的大多数情况下,你会在编辑器或 REPL(Read-Eval-Print-Loop,读-求值-打印-循环)中这样做。REPL 包含以下部分

  1. 读取一个表达式(一串字符)以生成 Clojure 数据。

  2. 求值从 #1 返回的数据,以生成结果(也是 Clojure 数据)。

  3. 通过将结果从数据转换回字符来打印结果。

  4. 循环回到开头。

#2 的一个重要方面是,Clojure 始终在执行之前编译表达式;Clojure **始终** 被编译为 JVM 字节码。没有 Clojure 解释器。

user=> (+ 3 4)
7

上面的框演示了求值表达式 (+ 3 4) 并接收结果。

在 REPL 中探索

大多数 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 中有用的额外函数:docfind-docapropossourcedir

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 基础知识,让你开始...。

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

不带换行符

print

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"

现在打印的结果是一个有效的形式,读取器可以再次读取。根据上下文,你可能更喜欢人类形式或数据形式。

测试你的知识

  1. 使用 REPL,计算 7654 和 1234 的总和。

  2. 将以下代数表达式重写为 Clojure 表达式:( 7 + 3 * 4 + 5 ) / 10

  3. 使用 REPL 文档函数,查找 remmod 函数的文档。根据文档比较所提供表达式的结果。

  4. 使用 find-doc,查找打印最近 REPL 异常堆栈跟踪的函数。