summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-25 18:39:43 +0800
committereltrac <hi@eltr.ac>2026-09-25 18:44:18 +0800
commit42641c83ca6946e6535ff2790374583e1f65deac (patch)
tree171f1b2fb1d54d12f738992f8cdca35a65228b8a /content/article
parent4140ef4b667087224db6c9fb8cf6de3d5e2561df (diff)
[draft] update
[draft] update
Diffstat (limited to 'content/article')
-rw-r--r--content/article/tutorial-clj-2.md.md248
1 files changed, 228 insertions, 20 deletions
diff --git a/content/article/tutorial-clj-2.md.md b/content/article/tutorial-clj-2.md.md
index 57d67931..2be4ef2f 100644
--- a/content/article/tutorial-clj-2.md.md
+++ b/content/article/tutorial-clj-2.md.md
@@ -4,7 +4,7 @@ subtitle = "Forms O' Plenty"
slug = 'tutorial-clj-2'
tags = ["Lisp","编程"]
categories = '茶歇蔻芥'
-date = 2026-09-23T23:15:12+08:00
+date = 2026-09-25 18:43:53
draft = true
toc = true
+++
@@ -28,9 +28,9 @@ toc = true
;; => clojure.lang.Symbol
```
-类型是 `Symbol`(符号)!这是 Lisp 中的一种特殊数据类型,和其他的原子不同,符号不是自求值(self-evaluating)的。意思是说,对 `"string"` 求值得到的是 `"string"` 本身,但对符号求值得到的是符号指向的其他值。如果要得到符号本身,就要用 `quote`。
+类型是 `Symbol`(符号)!这是 Lisp 中的一种特殊数据类型,和其他的原子不同,符号不是自求值(self-evaluating)的。意思是说,对 `"string"` 求值得到的是 `"string"` 本身,但对符号求值得到的是符号指向的其他值。
-符号指向的值可以是某个自求值对象,也可以是列表;这个列表可以是数据,也可以是个函数,还可以是宏。要定义一个符号也很简单,只需要用到 `def`。
+要定义一个符号很简单,只需要用到 `def`。
```clojure
(def my-symbol 123)
@@ -41,9 +41,6 @@ toc = true
my-symbol
;; => 123
-
-(eval 'my-symbol)
-;; => 123
```
可见符号和它指向的数据本身是等价的,对 `123` 求值和对 `my-symbol` 求值得到的结果都是 `123`。
@@ -52,8 +49,7 @@ my-symbol
```clojure
(def my-fn
- (fn [a b]
- (+ a b)))
+ (fn [a b] (+ a b)))
;; => #'user/my-fn
(fn [a b] (+ a b))
@@ -81,28 +77,20 @@ my-fn
之前我们提到对 `(eval (quote x))` 求值和对 `x` 本身求值是等价的,这里也成立。对 `my-fn` 求值返回的是个符号,对 `(eval 'my-fn)` 求值得到的也是个符号。只要形式的第一个元素是指向函数的符号,就能完成函数调用。
-Clojure 有个 `symbol` 函数,输入字符串,它会返回特定名称的符号。
-
-```clojure
-((eval (symbol "my-fn")) 1 2)
-;; => 1 2
-```
-
根据条件返回不同类型的符号也是可以的:
```clojure
(def a 1)
;; => #'user/a
-((eval (if (> a 1) '+ '-)) 2 1)
+((if (> a 1) + -) 2 1)
;; => 1
```
-好了好了,没人会这么写程序的。你现在知道,函数形式就是第一个元素为指向函数的符号的形式,对函数形式求值就是调用函数。还记得刚才让一个符号指向一个值的 `def` 吗?如果要用来声明函数,还有更简便的写法:
+好了好了,没人会这么写程序的。你现在知道,==函数形式就是操作符为指向函数的符号的形式==,对函数形式求值就是调用函数。还记得刚才让一个符号指向一个值的 `def` 吗?如果要用来声明函数,还有更简便的写法:
```clojure
(def my-fn
- (fn [a b]
- (+ a b)))
+ (fn [a b] (+ a b)))
;; 等价于
(defn my-fn [a b]
@@ -121,7 +109,7 @@ a
;; => 6
```
-我们对列表 `(1 2 3)` 做了累加操作,变成了 `6`,然后赋值给 `a`。函数也可以这么干,比如 `partial` 函数可以让我们提前填充函数的某些参数,然后返回新的函数:
+我们对列表 `(1 2 3)` 做了累加操作,变成了 `6`,然后赋值给 `a`。函数也可以这么干,比如 `partial` 就是对函数的操作,可以让我们提前填充函数的某些参数,然后返回新的函数:
```clojure
(def plus-one (partial + 1))
@@ -143,7 +131,227 @@ a
## 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>如果 `fn` 是函数的解释</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 里的集合数据类型,分别是我们已经见过的列表(list)和向量(vector),此外还有集合(set)和映射(map)。我们还会讲到如何操作这些集合,包括遍历和筛选等等。我们还会了解到惰性序列(Lazy sequence),然后你就可以写程序处理相对复杂的数据,并且能够到同时做到语法简洁和高性能。这些都是后话了。
+
+回见!
+