summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/article/common-lisp-start.md105
1 files changed, 80 insertions, 25 deletions
diff --git a/content/article/common-lisp-start.md b/content/article/common-lisp-start.md
index 6f868d71..aa88d753 100644
--- a/content/article/common-lisp-start.md
+++ b/content/article/common-lisp-start.md
@@ -1,25 +1,29 @@
+++
title = 'Just A Common Lisper'
-subtitle = ''
-slug = ''
-tags = ["Lisp"]
-categories = ''
-date = 2026-06-02T09:44:33+08:00
-draft = true
+slug = 'common-lisp-start'
+tags = ["Lisp","编程"]
+categories = '代码炼金术'
+date = 2026-06-02 17:53:10
+draft = false
toc = true
+endnotes = [
+ '第一小节的标题是对音乐剧《Hamilton》中《My Shot》的戏仿,原文是:*Enter: Me (He says in parentheses)*'
+]
+++
-最近在玩 [Common Lisp](https://lisp-lang.org/),由于中文博客几乎没有讨论,包括文档在内的英文资料都很零散——官网上只有简单的人上手介绍,而社区维护的[文档](https://lisp-docs.github.io/docs/getting-started)的某一页竟然写着 *Please help us fill this page*,我想这是因为 CL 的实现太多了,相关的网站而也很多——看来得自力更生了,也借此写一篇博客,既用作信息整理,也试图提高一些 Lisp 的讨论度吧。<!--more-->
+最近在玩 [Common Lisp](https://lisp-lang.org/),由于中文博客几乎没有讨论,包括文档在内的英文资料都很零散——官网上只有简单的上手介绍,而社区维护的[文档](https://lisp-docs.github.io/docs/getting-started)的某一页竟然写着 *Please help us fill this page*,我想这是因为 CL 的实现太多了,相关的网站很多,人力是分散的——看来得自力更生了,也借此写一篇博客,既用作信息整理,也试图提高一些 Lisp 的讨论度吧。<!--more-->
尽管 Common Lisp 的具体实现之间差异不会太大,但我想还是说明一下比较好:我使用的 Common Lisp 实现是 [Steel Bank Common Lisp](https://www.sbcl.org/),即 `sbcl`。
+文章没有什么主题,可以当作一篇 Lisp 杂谈,还有一些我对编程语言的思考,希望读者可以在文中找到一些有启发的观点和设计吧,不过我不保证什么。
+
## Enter: Common Lisp (He says in parentheses)
我的启蒙 Lisp 是 [Clojure](https://clojure.org/),这门语言最明显的特征是使用了 `()`(Parentheses)以外的其他括号,比如 `[]`(brackets) 和 `{}`(braces)。其中,`[]` 表示向量,也被大量地用在函数签名、`let` 绑定等语法上;`{}` 表示映射,放在花括号里的奇数个元素是键,偶数个元素是值。这么做的好处是极大地缩短了代码,坏处是…… 不够纯粹?
据我所知,Common Lisp 和 Scheme 都只有一种括号,语法非常简单,所有程序结构都用圆括号包裹,不需要额外记忆别的语法。几乎没有语法也是 Lisp 的特点之一,而 Clojure 的做法无疑引入了不少新语法。
-我目前还不能很好地回答这个问题,我只知道,在 Common Lisp 里不能像 Clojure 那样爽写隐射,很不习惯。
+我目前还不能很好地回答这个问题,我只知道,在 Common Lisp 里不能像 Clojure 那样爽写映射,很不习惯。
要在 Clojure 里表示键值对,可以这样写:
@@ -33,7 +37,7 @@ toc = true
两个参数都是原子的 `cons` 调用构造有序对(ordered pair),例如 `(cons 1 2)` 的值被表示为 `(1 . 2)`,其中的 `.` 表示这不是列表,而是一对值,这种类型的值被称作 *cons pair* 或者 *cons cell*。
-一个有序对的前半部分被称作 `car`,后半部分被称作 `cdr`,`(car (cons x y))` 返回 `x`,`(cdr (cons x y))` 返回 `y`。这两个词的意思分别是*寄存器编号的地址部分的内容*(Contents of the Address part of the Register)和*寄存器编号的减量部分的内容*(Contents of the Decrement part of the Register)。之所以这么命名,貌似是出于一些历史原因,最早的 Lisp 是在上世纪的 IBM 704 计算机上实现的,和硬件强相关。不过具体的背景我就不打算深究了,这不是词源学系列。
+一个有序对的前半部分被称作 `car`,后半部分被称作 `cdr`,`(car (cons x y))` 返回 `x`,`(cdr (cons x y))` 返回 `y`。这两个词的意思分别是*寄存器编号的地址部分的内容*(Contents of the Address part of the Register Number)和*寄存器编号的减量部分的内容*(Contents of the Decrement part of the Register Number)。之所以这么命名,貌似是出于一些历史原因,最早的 Lisp 是在上世纪的 IBM 704 计算机上实现的,和硬件强相关。不过具体的背景我就不打算深究了,这不是词源学系列。
当我们嵌套有序对,就构成了列表,例如 `(cons 42 (cons 69 (cons 613 nil)))` 构造以下的数据结构。最后一个 `nil`(空值)表示列表结束。
@@ -49,11 +53,11 @@ toc = true
(cons 'c 3)))
```
-这段示例代码来自 [Marin Atanasov Nikolov](https://dnaeon.github.io/common-lisp-lookup-tables-alists-and-plists/),其中 `'a` 表示对符号 `a` 的「引用」,以 `'` 开头的符号不会被求值(evaluate),而是会原样保留,可以用来表示键。
+这段示例代码来自 [Marin Atanasov Nikolov](https://dnaeon.github.io/common-lisp-lookup-tables-alists-and-plists/),其中 `'a` 表示对符号 `a` 的「引用」,以 `'` 开头的符号不会被求值,而是会原样保留,可以用来表示键。
要获取 `alist` 当中的某个有序对,可以用 `assoc` 函数。`(assoc 'a *alist*)` 会返回 `(A . 1)`[^1]。如果只想要键或者值,用 `cdr` 或者 `car` 就好了。此外,还可以用逆向关联 `rassoc`(reverse association),通过值来查找有序对,`(rassoc 1 *alist*)` 也会返回 `(A . 1)`。
-这个数据结构非常简单易懂。请思考一下,如果要给这个隐射添加新的关系,应该怎么做?
+这个数据结构没有引入新的语言机制,非常简单。请思考一下,如果要给这个映射添加新的关系,应该怎么做?
用 `cons` 把新的对加入到原来的列表中就行了,就像这样:`(cons (cons 'd 4) *alist*)`。还有另一个很好用的函数叫作 `acons`,它可以从第一和第二个参数创建新的有序对,然后再执行 `cons`,这样就可以少一层嵌套了:`(acons 'd 4 *alist*)`。不过这两个函数都不会修改原来的 `*alist*`,而是返回新的数据结构,编写函数式程序很合适;如果想要改变原来的变量,可以用 `push`,就像这样:`(push (cons 'd 4) *alist*)`
@@ -66,7 +70,7 @@ toc = true
有序对组成的列表看起来很符合直觉,不过从结构上看,`((A . 1) (B . 2))` 为什么不能变成扁平的 `(A 1 B 2)` 呢?我联想到数据库设计的 Schema-On-Write 和 Schema-On-Read,前者坚持写入提前定义好的结构化数据,数据的结构本身就应该表达明确的语义,后者则在读数据时才解析数据结构(比如可以把 JSON 字符串写进数据表里,读取之后再解析 JSON),更加灵活也降低了数据库设计的复杂度。
-Clojure 的 `{}` 语法就是这样的,Common Lisp 也可以做到,可以直接把键值对按照奇偶顺序写进列表里,这种列表叫作属性列表(Property List),即 `plist`。
+Clojure 的 `{}` 语法就是这样的,Common Lisp 也可以做到,可以直接把键值对按照奇偶顺序写进列表里,这种列表叫作属性列表(Property List),即 `plist`。从结构上看,`plist` 只是列表,不能表示映射关系,但在读取数据时,可以按照约定俗成的方式解析,此时它就有了映射关系。用 `getf` 就可以从 `plist` 中获取键对应的值。
```lisp
(let ((plist (list :a 1 :b 2)))
@@ -76,15 +80,11 @@ Clojure 的 `{}` 语法就是这样的,Common Lisp 也可以做到,可以直
其中 `:` 开头的值表示关键词(keyword),和 Clojure 等 Lisp 方言一样,类似无需提前定义的枚举类型,一般表示键值对的键。尽管这样一来和 Clojure 的差别就小了些,但还是要写个 `list` 函数。
-这么看来,Clojure 作为一门目前看来比 Common Lisp 抽象层次更高的语言,帮我隐藏了不少底层细节,用起来令人愉悦。Common Lisp 尽管在大多数时候也不需要接触这些细节,但在学习时会自然地接触到相关概念,体感上是更适合用来研究(满足好奇心)的语言。
-
-## Not all are functions and not all functional
-
-本节要来谈函数和函数式编程,按理说这两个东西不应该放在一起,是两个话题,~~但我想写这样一个好玩的标题~~。
+这么看来,Clojure 作为一门目前看来比 Common Lisp 抽象层次更高的语言,帮我隐藏了不少底层细节,用起来令人愉悦(比如 Clojure 中 `car` 对应 `first`,`cdr` 对应 `rest`)。Common Lisp 尽管在大多数时候也不需要接触这些细节,但在学习时会自然地接触到相关概念,体感上是更适合用来研究(满足好奇心)的语言。
-### Not all are functions
+## Not all are functions
-Lisp 的基石当然是列表,一般的列表都表示函数调用(至少看起来是函数引用),第一个列表元素是函数名,之后的元素都是参数。如果关注语言底下是什么,这么理解 Lisp 代码没有问题,加法 `(+ 1 2)` 是函数,逻辑运算 `(or t nil)` 是函数,赋值 `(setq *foo* "bar")` 也是函数——语法统一,非常美好。
+Lisp 的基石当然是列表,一般的列表都表示函数调用(至少看起来是这样),第一个列表元素是函数名,之后的元素都是参数。如果不关注语言底下是什么,这么理解 Lisp 代码没有问题,加法 `(+ 1 2)` 是函数,逻辑运算 `(or t nil)` 是函数,赋值 `(setq *foo* "bar")` 也是函数——语法统一,非常美好。
但事实**并非如此**,不是所有列表都是函数调用,有些东西无法通过函数实现。比方说 `if`,它接收三个参数:条件、条件为真时求值的表达式和条件为假时求值的表达式。
@@ -110,17 +110,17 @@ Lisp 的基石当然是列表,一般的列表都表示函数调用(至少看
显然是可以的,或者至少可以做到类似的形式,并且宏定义是可以写在标准库和第三方库里的,这样一来,就无须在解释器(或编译器)中编写特例。
-例如 `when` 和 `unless` 宏,会被展开为 `(if ... do nil)` 和 `(if ... nil do)`,而 Common Lisp(当然 Clojure 和不少 Lisp 方言都有这两个宏)的解释器和编译器不需要单独实现 `when` 或 `less`,只需要实现 `if`,剩下的只要交给宏就好了。
+回到 Common Lisp,`when` 和 `unless` 宏会被展开为 `(if ... do nil)` 和 `(if ... nil do)`,而 Common Lisp(当然 Clojure 和不少 Lisp 方言都有这两个宏)的解释器和编译器不需要单独实现 `when` 或 `less`,只需要实现 `if`,剩下的只要交给宏就好了。
当然,除了保持语言简洁,宏还能方便程序员拓展语言能力,用自己喜欢的方式编写代码而不必忍受语言限制。
-### Not all functional
+## Not all functional
-可能是因为我一开始读过的不少 Lisp 相关文章和书籍,都把 Lisp 和函数式编程联系起来,我一开始以为 Lisp 都是纯函数式的(思来想去,应该是鲍勃大叔的《整洁架构之道》在讲函数式编程范式是,把它称作「不少 Lisp 方言实现的范式」,误解就由此产生了),我还因为 Haskell 是纯函数式的编程语言,把它误以为是 Lisp 方言,闹过一些笑话。
+可能是因为我一开始读过的不少 Lisp 相关文章和书籍,都把 Lisp 和函数式编程联系起来,我一开始以为 Lisp 都是纯函数式的(思来想去,应该是鲍勃大叔的《整洁架构之道》把函数式编程范式称作「不少 Lisp 方言实现的范式」,误解就由此产生了),我还因为 Haskell 是纯函数式的编程语言,把它误以为是 Lisp 方言,闹过一些笑话。
Clojure 的确是一门整体上更偏好函数式编程的语言(关于什么是函数式,可以读[这篇文章](/library/2026/functional-design/#什么是函数式编程)),比较直观的体现是,几乎所有的 Clojure 教程和示例代码都没有定义变量然后再赋值的教程。默认情况下,几乎所有变量都是不可变的,包括用 `let` 局部绑定的变量。
-而 Common Lisp 似乎不强调这点,尽管有些麻烦,但 Common Lisp 可以轻易地给变量赋值,不过这种变量被称作动态变量(dynamic variable)。
+而 Common Lisp 似乎不强调这点,Common Lisp 可以轻易地给变量赋值,不过这种变量被称作动态变量(dynamic variable)。
```lisp
(defparameter *dynvar* 1)
@@ -148,9 +148,64 @@ Clojure 的确是一门整体上更偏好函数式编程的语言(关于什么
>
> 尽管 let 的作用并不模糊,它发挥作用的方式却被刻意地留白了。let 的作用与它的作用方式分离。通过某种方式,let 需要提供用来储存指向值的指针的数据结构。
-据后文所言,Common Lisp 编译器会根据情况编译 `let`
+据后文所言,Common Lisp 编译器会根据情况处理 `let`,选取最好的「存储」方案。程序员也可以在代码中提供更多的信息,这样 Lisp 编译器就能编译出效率更好的代码。
+
+```lisp {hl_lines=2}
+(defun register-allocated-fixnum ()
+ (declare (optimize (speed 3) (safety 0)))
+ (let ((acc 0))
+ (loop for i from 1 to 100 do
+ (incf (the fixnum acc)
+ (the fixnum i)))
+ acc))
+```
+
+这段代码中的 `declare` 就是给编译器的优化「建议」,编译时,这段函数会把 `let` 里的数据加载到 CPU 寄存器中,效率会很高。
+
+书中还提到了 `lambda`(毕竟书名就是《Let Over Lambda》,书名其实是书中一种重要的 Lisp 编程模式,*也缩写成 LOL*),这里的 `lambda` 指代的是 λ 演算(λ-calculus),现代的非 Lisp 编程语言也把这个特性拿取用了,成了所有程序员都熟悉的「匿名函数」。
+
+书中提到,`let` 的一种实现方式就是 `lambda`,但并没有展开,此时我们可以把目标转向《计算机程序的构造与解释》(SICP)一书。
+
+> The first part of the let expression is a list of name-expression pairs. When the let is evaluated, each name is associated with the value of the corresponding expression. The body of the let is evaluated with these names bound as local variables. The way this happens is that the let expression is interpreted as an alternate syntax for
+>
+> ```scheme
+> ((lambda (<var1> ...<varn>)
+> <body>)
+> <exp1>
+> ...
+> <expn>)
+> ```
+>
+> No new mechanism is required in the interpreter in order to provide local variables. A let expression is simply syntactic sugar for the underlying lambda application.
+
+这段代码首先使用 `lambda` 创造了一个匿名函数,这个函数有 n 个形参,同时,这段代码直接调用了这个函数,传入了 n 个参数。对 Lisp 不熟悉的读者(真的会有那样的读者读到这里吗?)可以参考 JavaScript 的立即执行函数。
+
+```JavaScript
+(function(a, b) {
+ return a + b
+})(1, 2);
+// => 3
+```
+
+这相当于:
+
+```scheme
+((lambda (a b) (+ a b)) 1 2)
+
+;; 相当于
+(let ((a 1) (b 2))
+ (+ a b))
+```
+
+要注意的是,SICP 使用的是 Scheme 而非 Common Lisp,不过你也看到了,这两门语言其实有不少相似之处。区别之一当然就是,Common Lisp 不指定 `let` 的实现方式,编译器可以做各种优化;而 Scheme 中,`let` 就是 `lambda` 的语法糖。
+
+后者当然是更函数式的——有什么比创建一个立即执行的匿名函数以实现局部变量更函数式的?从效率方面考虑的话,调用函数就要在内存中创建函数栈帧,而经过优化的 Common Lisp 程序可以把数据直接存放在 CPU 寄存器里,这在执行效率上是更好的。
+
+此外,Common Lisp 还能够实现 OOP——用 `defgeneric` 和 `defmethod` 实现多态,用 `defclass` 定义类,用 `make-instance` 实例化类——尽管函数式和面向对象并不冲突,但这也说明 Common Lisp 不那么注重函数式风格,它更像是一门什么都能干的语言。
+
+---
-## Footnotes
+后面的知识,之后再来探索吧。
[^1]: Common Lisp **不**大小写敏感,所有代码最终都会被转换为大写字母。