1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
+++
title = 'Coffee Break Clojure, Forms'
subtitle = "Forms O' Plenty"
slug = 'clj2'
aliases = '/article/tutorial-clj-2/'
tags = ["Lisp","Clojure","编程"]
categories = '茶歇蔻芥'
date = 2026-09-26 10:06:41
draft = false
toc = true
fedi = 'https://akk.eltr.ac/objects/f4ccc44c-7fdd-4751-b781-3097f2938de4'
+++
[上一篇](/article/tutorial-clj-1/)我们配置好了 REPL,可以随时开始编写 Clojure 代码并执行。[第零篇](/article/tutorial-clj-0/)提到,被求值(Eval)的列表叫作形式(form),形式的第一个元素是操作符,剩余的是参数。根据操作符的不同,形式被分为函数形式、Lambda 形式、宏形式和特殊形式。今天的文章就来介绍这几种形式。
## 函数形式
不难理解,函数形式就是操作符为函数的形式,比如这个 `println` 形式:
```clojure
(println "Hello World")
```
我们知道 `"Hello World"` 是字符串,那 `println` 是什么类型?在其他语言里我们可能很少考虑函数名本身的类型,但别忘了 Lisp 的「代码即数据」本质。要把待求值的形式当作数据而非形式处理,只需要把它 `quote` 起来。
这里要引入 Clojure 的 `type` 函数,它返回输入值的类型。我们不妨把刚才的形式 `quote` 起来,然后获取他的第一个元素 `println`,看看它是什么类型。
```clojure
(type (first '(println "Hello World")))
;; => clojure.lang.Symbol
```
类型是 `Symbol`(符号)!这是 Lisp 中的一种特殊数据类型,和其他的原子不同,符号不是自求值(self-evaluating)的。意思是说,对 `"string"` 求值得到的是 `"string"` 本身,但对符号求值得到的是符号指向的其他值。
要定义一个符号很简单,只需要用到 `def`。
```clojure
(def my-symbol 123)
;; => #'user/my-symbol
123
;; => 123
my-symbol
;; => 123
```
可见符号和它指向的数据本身是等价的,对 `123` 求值和对 `my-symbol` 求值得到的结果都是 `123`。
接下来试试让符号指向函数:
```clojure
(def my-fn
(fn [a b] (+ a b)))
;; => #'user/my-fn
(fn [a b] (+ a b))
;; => #object[user$eval170$fn__171 0x6f8e0cee "user$eval170$fn__171@6f8e0cee"]
my-fn
;; => #object[user$my_fn 0x3df978b9 "user$my_fn@3df978b9"]
(my-fn 1 2)
;; => 3
```
`fn` 是个宏(macro),稍后我会讲到它为什么必须是个宏而不能是函数。你可以把它理解为其他语言里的匿名函数,或者 Lambda 表达式。实际上 `fn` 就是 Common Lisp 和 Scheme 里的 `lambda`,不过换了个名字。
直接对 `fn` 形式求值得到的是形如 `#object[...]` 的字符串,这个字符串是 JVM 上某个数据的文字表示。像函数这种数据没办法像字符串和数字那样直接表示出来,于是就用这种标记表示。
输出这种标记对程序而言没有意义,使用函数的正确方式当然是调用它。上述例子中的 `(my-fn 1 2)` 就是个函数形式,第一个元素是指向函数的符号,剩下的数据作为参数传递给符号指向的函数。
既然符号本身也是数据,那么如果形式的第一个元素不是符号,而是返回符号的函数调用,能否正常调用函数呢?还记得 `eval` 本身也是个函数吗?
```clojure
((eval 'my-fn) 1 2)
;; => 3
```
之前我们提到对 `(eval (quote x))` 求值和对 `x` 本身求值是等价的,这里也成立。对 `my-fn` 求值返回的是个符号,对 `(eval 'my-fn)` 求值得到的也是个符号。只要形式的第一个元素是指向函数的符号,就能完成函数调用。
根据条件返回不同类型的符号也是可以的:
```clojure
(def a 1)
;; => #'user/a
((if (> a 1) + -) 2 1)
;; => 1
```
好了好了,没人会这么写程序的。你现在知道,==函数形式就是操作符为指向函数的符号的形式==,对函数形式求值就是调用函数。还记得刚才让一个符号指向一个值的 `def` 吗?如果要用来声明函数,还有更简便的写法:
```clojure
(def my-fn
(fn [a b] (+ a b)))
;; 等价于
(defn my-fn [a b]
(+ a b))
```
写 Clojure 函数的时候你多半只会用到 `defn`,但不要忘了函数名和变量名一样,都只是符号而已。
记住这个原理你能做到很多有趣的事情。比方说,在其他语言里我们经常对值做一些操作,然后赋值给变量,就像这样:
```clojure
(def a (reduce + '(1 2 3)))
;; => #'user/a
a
;; => 6
```
我们对列表 `(1 2 3)` 做了累加操作,变成了 `6`,然后赋值给 `a`。函数也可以这么干,比如 `partial` 就是对函数的操作,可以让我们提前填充函数的某些参数,然后返回新的函数:
```clojure
(def plus-one (partial + 1))
;; => #'user/plus-one
(plus-one 3)
;; => 4
((partial + 1) 3)
;; => 4
```
我们对函数 `+` 做了 `partial` 操作,给它提前填上了一个参数 `1`。调用得到的新函数就相当于调用 `(+ 1 ...)`。在其他语言里,要做到类似的事情,你可能要这么写:
```clojure
(defn plus-one [num]
(+ 1 num))
```
你可能注意到了 Clojure 函数和非 Lisp 语言的另一个区别:没有 `return` 语句。函数如果有多个并列的形式,就会以最后一个形式的值作为函数的返回值。
做个习题,下面这段函数调用为什么返回了 `nil`?
```clojure
(defn chatty-add [a b]
(let [result (+ a b)]
result
(println result)))
(chatty-add 1 2) ;; => nil
```
## Lambda 形式
Lambda 形式类似 JavaScript 里的立即执行函数,也就是定义一个匿名函数,然后立马执行它,这样就可以限定变量的作用域。
```js
(function (a, b) {
let c = a + b
return c
})(1, 2)
// => 3
```
如果是 Clojure,就要这么写:
```clojure
((fn [a b] (+ a b)) 1 2)
;; => 3
```
Lambda 形式的操作符不是指向函数的符号,就是函数本身。
在 Scheme(另一门 Lisp 方言)里要这么写:
```scheme
((lambda (a b) (+ a b)) 1 2)
;; => 3
```
在 Scheme 里,这种写法有个语法糖,叫 `let`:
```scheme
(let ((a 1) (b 2))
(+ a b))
```
没错,这和 JavaScript 和 Rust 等语言用于声明变量的 `let` 很类似,但 Scheme 中 `let` 仅仅是立即执行函数的语法糖。不过,在 Clojure 中,`let` 是个特殊形式,有专门的[解析器](https://github.com/clojure/clojure/blob/07adae6d24a0efcb4e6b39577c473b61df9d5f5b/src/jvm/clojure/lang/Compiler.java#L6902),我会在后文展开。
## 宏形式
前面我一直在用加减法做例子,现在不妨来看看逻辑运算。Clojure 中表示与、或、非运算的操作符分别是 `and`、`or` 和 `not`。其中 `not` 是个非常简单的函数:
```clojure
(defn not [x]
(if x false true))
;; 如果 x 为真,返回 false
;; 如果 x 为假,返回 true
```
但 `and` 和 `or` 不可以是函数,必须是宏。为什么?我们不妨假设他们是函数,看看会发生什么。
```clojure
(let [a 1 b 2]
(or (> b 1)
(> a 1)))
```
首先要对 `or` 函数的所有参数求值,得到值之后才能调用函数。`(> b 1)` 为 `true`,`(> a 1)` 为 `false`,然后得到 `(or true false)`,最后算出值为 `true`。
你发现了吗?我们做了一步完全多余的运算。根本不需要计算 `(> a 1)` 为 `false`,只要算出 `(> b 1)` 为 `true`,就可以算出整个 `or` 形式的值为 `true`,因为逻辑运算或只要求其中一个值为真。
如果 `or` 是函数,就必须做那一步多余的运算,否而得不到值,无法调用函数。然而,有些运算是有副作用的,比如从消息队列中取出一条消息,会导致其他人无法再次从队列中取到它,若 `or` 运算丢弃了这条消息,数据就丢失了。
那怎么办呢?有什么办法可以让参数不被预先求值吗?
答案就是宏(macro)。宏这个机制被不少语言借鉴,比如 Rust 和 Haskell(在 Haskell 里叫 Template,模板),其实本质就是对还没求值的列表做预处理。
定义宏就是定义一系列对列表做变换的规则,宏作为形式的操作符时,会先进行宏展开(macroexpand),然后对展开的结果求值。宏展开之前,所有的列表都不会被求值。
我们来看看 `or` 这个宏的定义:
```clojure
(defmacro or
([] nil)
([x] x)
([x & next]
`(let [or# ~x]
(if or# or# (or ~@next)))))
```
没有参数时n展开为 `nil`,只有一个参数时展开为这个参数本身,有多个参数时会被展开为嵌套在 `let` 里的 `if`,我们看到宏还能像函数一样递归,因为 `or` 的定义包含了 `or` 本身。
这看起来有点难以理解,我们不妨手动展开一下:
```clojure
(macroexpand '(or (> b 1) (> a 1)))
;; => (let* [or__5600__auto__ (> b 1)] (if or__5600__auto__ or__5600__auto__ (clojure.core/or (> a 1))))
;; 整理得到:
(let* [or__5600__auto__ (> b 1)]
(if or__5600__auto__
or__5600__auto__
(clojure.core/or (> a 1))))
;; clojure.core/or 就是包含命名空间的 or,指向同一个宏
;; 由于单参数的 or 展开为参数本身,所以可以得到:
(let* [or__5600__auto__ (> b 1)]
(if or__5600__auto__
or__5600__auto__
(> a 1)))
;; 为了方便理解,我们再把 let 删掉:
(if (> b 1) (> b 1) (> a 1))
```
`if` 是个特殊形式,和 `let` 一样有自己的解析器。当 `if` 计算 `(> b 1)` 得到 `true` 时,就会选择第二个参数求值,第二参数是 `(> b 1)`,得到 `true`,于是返回 `true`;第三个参数 `(> a 1)` 没有被求值,我们不知道他是 `true` 还是 `false`。
还记得一开始的 `fn` 吗?如果 `fn` 是个函数,下面这段表达式会变成什么样子?
```clojure
(fn [a b c]
(println a b c)
(+ a b c))
```
<details>
<summary>如果 <code>fn</code> 是函数的解释</summary>
首先对 `[a b c]` 求值,解释器会发现 `a` 这个符号不存在,然后报错。
</details>
关于怎么自定义宏,就放在之后的文章再讲了。
## 特殊形式
前面提到 `if` 和 `let` 都是特殊形式,可以理解为其他语言里的保留字。这些符号有特殊的解释规则,或者编译规则。==特殊形式就是由编译器和解释器实现的形式==。函数和宏都是用户定义的,只不过有的定义在标准库里。
有趣的是,Clojure 的 `let` 最外层其实是个宏,而 `let*` 才是特殊形式,只不过 `let` 最终会被展开为 `let*`。
```clojure
(macroexpand '(let [a 1 b 1] (+ a b)))
;; => (let* [a 1 b 1] (+ a b))
```
看起来 `let` 被展开之后仅仅是把 `let` 替换成了 `let*`,为什么要套一层宏呢?
这次我们故意把 `let` 写错,看看会发生什么。
```clojure
(macroexpand '(let [a 1 b] (+ a b)))
;; Syntax error macroexpanding clojure.core/let at (REPL:1:1).
;; [a 1 b] - failed: even-number-of-forms? at: [:bindings]
;; spec: :clojure.core.specs.alpha/bindings
```
报错了,错误在 `[a 1 b]`,失败原因是 `let` 里写的符号绑定应该是偶数个。这个语法检查并不是编译器实现的,而是宏定义的:
```clojure
(defmacro let
[bindings & body]
(assert-args
(vector? bindings) "a vector for its binding"
(even? (count bindings)) "an even number of forms in binding vector")
`(let* ~(destructure bindings) ~@body))
```
`assert-args` 是标准库内部的宏,用来断言(assert)参数是否符合规范(spec)。在 `let` 的宏定义中,它要求 `bindings`(符号绑定)是向量(也就是包裹在 `[]` 里的数据结构),并且是偶数个。断言失败就不会继续展开宏,而是会报错。
如果没有问题,`let` 就会被展开为 `let*`,展开之前还对 `bindings` 执行了 `destructure` 操作。这是解构,解构是 Clojure 的语法特性,Python 和 JavaScript 也有解构:
```js
let anObject = { a: 1, b: 2, c: 3 }
const {a, b, c} = anObject
console.log(a, b, c) // 1 2 3
```
上述代码用 `{a, b, c}` 代替了变量名,用来从 `anObject` 这个对象里提取和变量名同名的属性。这就是解构操作。
Clojure 没有用编译器实现解构,而是用 `destructure` 函数实现的,这个解构函数在 `let` 宏展开时调用。以下是用 `let` 解构的例子:
```clojure
(def a-map {:a 1 :b 2 :c 3})
(let [{:keys [a b c]} a-map]
(println a b c))
;; (out) 1 2 3
;; => nil
```
如果对上面的 `let` 形式做宏展开,就会得到:
```clojure
(let* [map__147 a-map
map__147 (if (seq? map__147) (if (next map__147) (clojure.lang.PersistentArrayMap/createAsIfByAssoc (to-array map__147)) (if (seq map__147) (first map__147) clojure.lang.PersistentArrayMap/EMPTY)) map__147)
a (get map__147 :a)
b (get map__147 :b)
c (get map__147 :c)]
(println a b c))
```
正因为 `let` 本身是宏,而非单独的编译器实现,Clojure 的 `let` 和其他 Lisp 的 `let` 比起来才好用不少。在 Common Lisp 和 Scheme 中,`let` 和 `let*` 的作用不同,比方说下面这段代码是有问题的:
```lisp
(let ((a 1) (b 2) (c (+ a b)))
(+ a b c))
; in: LET ((A 1) (B 2) (C (+ A B)))
; (+ A B)
;
; caught WARNING:
; undefined variable: COMMON-LISP-USER::A
;
; caught WARNING:
; undefined variable: COMMON-LISP-USER::B
; ...
```
求值 `(+ a b)` 的时候,Common Lisp 解释器会认为 `a` 和 `b` 没有被绑定,因为 `(c (+ a b))` 和 `a`、`b` 的绑定在同一个 `let` 里。如果要使用在同一个 `let` 定义的其他符号,就要用 `let*`:
```lisp
(let* ((a 1) (b 2) (c (+ a b)))
(+ a b c))
; 6
```
《[A Scheme Primer](https://files.spritely.institute/papers/scheme-primer.html#fn.more-lets)》里写到,如果 Scheme 的规范要从头开始指定,最好要把 `let`、`let*` 还有 `letrec`(允许递归引用符号自己的 `let`)都合成同一个。Clojure 作为较新的一门 Lisp 方言,少了很多历史遗留问题。
---
## 最后
现在你应该知道求值 Clojure 形式时,解释器底下大概都发生了什么,你应该也熟悉了一些基本的函数、宏和特殊形式,比如 `let` `if` 和 `fn`,可以写一些简单的 Clojure 程序了。
其实除了本文提到的四种形式,Clojure 还可以用特定类型的数据作为操作符(比如用一个集合做操作符,用于查看参数是否存在于集合内),也可以当作一种类型的形式。这要留到下篇讲了。
下一篇我们要了解 Clojure 里的集合数据类型,分别是我们已经见过的列表(list)和向量(vector),此外还有集合(set)和映射(map)。我们还会讲到如何操作这些集合,包括遍历和筛选等等。我们还会了解到惰性序列(Lazy sequence),然后你就可以写程序处理相对复杂的数据,并且能够到同时做到语法简洁和高性能。这些都是后话了。
回见!
|