summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-25 22:24:28 +0800
committereltrac <hi@eltr.ac>2026-09-25 22:24:28 +0800
commit1e374581fad4a7f967526aade498932508f13657 (patch)
tree0d8c1816674b23f36a76d789598b996dd38cb420 /content/article
parent42641c83ca6946e6535ff2790374583e1f65deac (diff)
[draft] update
Diffstat (limited to 'content/article')
-rw-r--r--content/article/tutorial-clj-2.md (renamed from content/article/tutorial-clj-2.md.md)2
-rw-r--r--content/article/tutorial-clj-3.md258
2 files changed, 260 insertions, 0 deletions
diff --git a/content/article/tutorial-clj-2.md.md b/content/article/tutorial-clj-2.md
index 2be4ef2f..3bfd5983 100644
--- a/content/article/tutorial-clj-2.md.md
+++ b/content/article/tutorial-clj-2.md
@@ -351,6 +351,8 @@ Clojure 没有用编译器实现解构,而是用 `destructure` 函数实现的
现在你应该知道求值 Clojure 形式时,解释器底下大概都发生了什么,你应该也熟悉了一些基本的函数、宏和特殊形式,比如 `let` `if` 和 `fn`,可以写一些简单的 Clojure 程序了。
+其实除了本文提到的四种形式,Clojure 还可以用特定类型的数据作为操作符,也可以当作一种类型的形式。这要留到下篇讲了。
+
下一篇我们要了解 Clojure 里的集合数据类型,分别是我们已经见过的列表(list)和向量(vector),此外还有集合(set)和映射(map)。我们还会讲到如何操作这些集合,包括遍历和筛选等等。我们还会了解到惰性序列(Lazy sequence),然后你就可以写程序处理相对复杂的数据,并且能够到同时做到语法简洁和高性能。这些都是后话了。
回见!
diff --git a/content/article/tutorial-clj-3.md b/content/article/tutorial-clj-3.md
new file mode 100644
index 00000000..237d8527
--- /dev/null
+++ b/content/article/tutorial-clj-3.md
@@ -0,0 +1,258 @@
++++
+title = 'Coffee Break Clojure, Vol.3'
+subtitle = '只用圆括号怎么够?'
+slug = 'tutorial-clj-3'
+tags = ["Lisp","编程"]
+categories = '茶歇蔻芥'
+date = 2026-09-25T20:17:41+08:00
+draft = true
+toc = true
++++
+
+[上一篇](/article/tutorial-clj-2)我们讨论了不同类型的形式,了解了函数、Lambda、宏和特殊形式的皮毛,也学会了用一些基本的运算符、`let` 和 `if` 等等。今天的文章要讨论集合数据类型。不过在此之前,我们还是先看看普通的数据类型有哪些吗?<!--more-->
+
+## 你已经知道和你可能不知道的
+
+和大部分编程语言一样,Clojure 支持数字、浮点数、字符串和布尔值。
+
+由于 Clojure 是运行在 JVM 上的,所以 Java 这些数据类型实际上对应一个 Java 类。数字是 `Long` 而不是 `Int`,前者能表示的最大数是 2 的 63 次方减 1,也就是有 64 位,第一位是符号位,后者是 32 位。和 Java 一样,Clojure 没有无符号数(unsigned int)。
+
+Clojure 还能在这些类型上调用 Java 的方法,比如 `String.getBytes()`。
+
+```clojure
+(seq (.getBytes "Hello"))
+;; => (72 101 108 108 111)
+(type (first (seq (.getBytes "Hello"))))
+;; => java.lang.Byte
+```
+
+Clojure 也没有用更小的 `Float`,也是用了双精度浮点数 `Double`。
+
+```clojure
+(type 1.1)
+;; => java.lang.Double
+```
+
+Clojure 是门动态类型语言,一般不需要考虑太多类型问题,不过知道这些类型底层是什么样的也挺好。
+
+值得注意的是 Clojure 还有个很常用的自定义类型,叫作关键词(keyword)。
+
+```clojure
+(type :keyword)
+;; => clojure.lang.Keyword
+```
+
+关键词类似字符串,不过是以 `:` 开头,中间不能有空格。其实他更像其他语言里的枚举类型(enums),但无需提前定义。关键词不能像字符串一样做裁剪、找子串、当作字符数组遍历之类的操作,就是一个整体。
+
+关键词可以用在大部分需要枚举类型的场合,也用来表示映射(map)里的键(key)。
+
+其他的非集合类型还有我们上一篇讨论过的符号(Symbol)。
+
+## 圆括号和列表
+
+我们在之前就见过列表(list),最普通的集合数据类型。
+
+```clojure
+(list "string" 1 :item true)
+;; or
+'("string" 1 :item true)
+```
+
+和许多动态类型语言一样,列表可以包含不同类型的数据。
+
+注意,上面这个例子中两种创建列表的方式有区别,我在[第零篇](/article/tutorial-clj-0)留了个习题,现在公布答案:
+
+```clojure
+(list 1 (+ 1 1) 3)
+;; => (1 2 3)
+'(1 (+ 1 1) 3)
+;; 猜猜这段代码会得到什么结果?
+;; (1 (+ 1 1) 3)
+```
+
+`quote` 会「冻结」列表内的一切,嵌套的形式也不会被求值。我们在上一篇讲到,调用函数前需要把所有参数都求值。`list` 就是个函数,`(+ 1 1)` 求值得到 `2` 才会被传入 `list`。
+
+在更「原教旨」的 Lisp 中,`list` 是 `cons` 的一种简便写法。`cons` 的意思是 *construct*,被称作「列表构造函数」。一个 `cons` 只能造一个有序对,`(cons 1 2)` 会返回 `(1 . 2)`,中间的点标记表示这是个 *cons* 对(叫 *cons pair* 或者 *cons cell*)。
+
+`cons` 构造列表的方式有点像个链表,`cons` 左边存放的是列表的一个元素,右边存放的是下一个 *cons* 对,如果列表结束了,右边存的就是 `NIL`。
+
+```lisp
+(cons 42 (cons 69 (cons 613 nil)))
+;; 等价于
+(list 42 69 613)
+```
+
+![](https://img.eltr.ac/geedeapro/article/common-lisp-start/Cons-cells.svg.png)
+{.dark:invert}
+
+虽然 Clojure 的 `cons` 也可以用相同的方式构造列表,但 *cons* 对却不存在。`cons` 的第二个函数只能是序列,不能是原子。
+
+```clojure
+(cons 1 2)
+;; Execution error (IllegalArgumentException) at user/eval30 (REPL:1).
+;; Don't know how to create ISeq from: java.lang.Long
+```
+
+可以说 Clojure 弱化了 `cons` 作为列表构造函数的身份,`cons` 只被用来「把数据放进列表的前面」。
+
+```clojure
+(cons 1 '(2 3))
+;; => (1 2 3)
+```
+
+Clojure 的列表叫作持久化列表(PersistentList),Clojure 的所有集合数据类型几乎都是持久化(Persistent)且不可变(Immutable)的。
+
+```clojure
+(type '(1))
+;; => clojure.lang.PersistentList
+```
+
+不可变很好理解,对数据做操作只会产生新的数据,而不会修改原来的数据,原来的数据还保存在内存的同一个地方。那什么是[持久化](https://zh.wikipedia.org/wiki/%E5%8F%AF%E6%8C%81%E4%B9%85%E5%8C%96%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84)呢?意思是数据被修改后,历史的版本还保留着并且还**能被访问**。
+
+不妨看看 Clojure 是怎么实现 `PersistentList` 的:
+
+```java
+public class PersistentList extends ASeq implements IPersistentList, IReduce, List, Counted {
+
+private final Object _first;
+private final IPersistentList _rest;
+private final int _count;
+```
+
+注意看 `_first` 和 `_rest` 这两个私有变量。`_first` 的类型是 `Object`,在 Java 中,这是所有类的基类,作用类似于 Go 的 `any`,任何类型传入都能通过类型检查。`_rest` 的类型是 `IPersistentList`,这是持久化列表的接口,`PersistentList` 就实现了这个接口。
+
+不难看出这和 *cons* 非常类似:`_first` 存的是列表节点的值,`_rest` 存的是下一个值的「指针」。在原教旨 Lisp 里,`_first` 和 `_rest` 分别对应 `car` 和 `cdr`(读作 *coulder*)[^1]。
+
+我们来看看 `PersistentList` 是怎么实现 `add()` 的:
+
+```java
+public void add(int index, Object element){
+ throw new UnsupportedOperationException();
+}
+```
+
+嗯?直接抛出异常了?
+
+令人费解,而且有违反里氏替换原则的嫌疑。仔细想想,Clojure 的确没有给列表直接添加元素的方法,我们用的是 `cons` 和 `conj`(后者与 `cons` 相反,是在序列的尾部添加元素)。那 `cons` 是怎么实现的?
+
+```java
+public Cons(Object first, ISeq _more){
+ this._first = first;
+ this._more = _more;
+}
+```
+
+这下就清晰了,求值 `(cons 1 '(2 3))` 得到 `(1 2 3)`,原来的 `1` 和 `(2 3)` 都留在原地,我们只是创建了一个 `Cons`,指向那两个值,而 `Cons` 本身也是个序列,可以再用它创建新的 `Cons` 或者其他序列(`Cons` 是 `ASeq` 的子类,也就实现了 `ISeq`,序列接口,`PersistentList` 也是序列)。
+
+这种构造持久化列表的方式叫作结构共享(Structural Sharing),既做到了不可变,又没有在内存中管理大量的数据副本。本质上,一切都是指针。
+
+由于不可变,所以列表的大小可以作为不变的属性保存起来,回去看 `PersistentList` 的 Java 代码,`_first` 和 `_rest` 下面就是 `_count`。Clojure 的所有集合数据类型都能用 `count` 函数获取其长度,并且这个算法的时间复杂度是 *O(1)*,因为只需要 `return _count`。此外,有很多操作都因为结构共享变得非常高效,Clojure 的性能在 Lisp 中算很好的那一类。
+
+至于不可变还有什么好处,我们之后谈函数式编程再提。
+
+## 方括号和向量
+
+理解一门 Lisp 中的列表如何构造,可以说就理解了它的核心。我们知道列表是持久化且不可变的,由 `Cons` 和指向其他 `Cons` 的「指针」构成。
+
+向量基本上和列表一致,但向量中所有元素都有索引(index),这点和 JavaScript 的数组很像,写起来也和 JavaScript 数组很像,也可以存放不同类型的数据。
+
+```clojure
+[1 "I'm a vector" :vector true]
+```
+
+列表和向量都可以用 `first`、`rest` 和 `cons` 函数。
+
+```clojure
+(first [1 2 3 4]) ;; => 1
+(first '(1 2 3 4)) ;; => 1
+
+(rest [1 2 3 4]) ;; => (2 3 4)
+(rest '(1 2 3 4)) ;; => (2 3 4)
+
+(cons 1 [2 3 4]) ;; => (1 2 3 4)
+(cons 1 '(2 3 4)) ;; => (1 2 3 4)
+
+;; 当然还支持其他操作序列的函数
+;; 列表和向量都可以通用
+```
+
+你可能发现执行了序列操作之后,向量的方括号变成了圆括号。不必担心,它实际上还是向量,行为是一致的。
+
+```clojure
+(type (rest [1 2 3 4]))
+;; => clojure.lang.PersistentVector$ChunkedSeq
+```
+
+向量和列表最大的不同点在于,可以用索引访问向量的元素,但列表没有索引。
+
+```clojure
+(get [1 2 3 4] 2) ;; => 3
+(get '(1 2 3 4) 2) ;; => nil
+```
+
+向量和列表的另一个区别,尽管他们都是连续的(sequential),但只有列表是序列(sequence)。用 `seq?` 检查数据是不是序列的时候,向量会返回 `false`。
+
+```clojure
+(seq? '(1 2)) ;; => true
+(seq? [1 2]) ;;=> false
+```
+
+如果希望向量也返回 `true`,就要检查一个集合有没有实现 `Sequential` 接口。
+
+```clojure
+(sequential? '(1 2)) ;; => true
+(sequential? [1 2]) ;; => true
+```
+
+有意思的是,把 `seq?` 的问号去掉,你也能让向量返回一个真值。
+
+```clojure
+(if (seq [1 2])
+ "I am a seq"
+ "I am not a seq")
+;; => "I am a seq"
+```
+
+`seq` 的作用是把可以转换为序列的数据结构转换为序列,字符串也可以转换成序列。
+
+```clojure
+(seq "string")
+;; => (\s \t \r \i \n \g)
+;; \ 用来表示 Clojure 中的字符
+```
+
+集合只要不是空的,都会返回一个正常的序列,会被条件判断当作真值。因此 `seq` 经常被用来检查一个集合是否不为空;相反,`emtpy?` 用来检查一个集合是否为空。
+
+序列(sequence)是只支持线性访问,不支持随机访问的集合,可以当作列表理解。序列的行为和列表几乎是一致的,它也没有索引。
+
+```clojure
+(get (seq [1 2]) 1)
+;; => nil
+```
+
+最后,向量其实还继承了 `AFn` 类,可以当成函数使用。
+
+```clojure
+([1 2 3] 0) ;; => 1
+;; 等价于:
+(get [1 2 3] 0) ;; => 1
+```
+
+这样就省掉了写 `get` 的工夫。
+
+## 花括号和映射
+
+Clojure 的映射写起来和 JSON 很像。
+
+```clojure
+{:key1 "val1"
+ :key2 {:subkey1 2
+ :subkey2 [1 2 3]}
+ :key3 nil}
+```
+
+
+
+## 井号和集合
+
+[^1]: 可参考:《[Just A Common Lisper](/article/common-lisp-start/)》