diff options
| author | eltrac <hi@eltr.ac> | 2026-09-25 22:24:28 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-09-25 22:24:28 +0800 |
| commit | 1e374581fad4a7f967526aade498932508f13657 (patch) | |
| tree | 0d8c1816674b23f36a76d789598b996dd38cb420 /content/article | |
| parent | 42641c83ca6946e6535ff2790374583e1f65deac (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.md | 258 |
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) +``` + + +{.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/)》 |
