惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - Franky
博客园 - 聂微东
V
Visual Studio Blog
I
InfoQ
罗磊的独立博客
Security Latest
Security Latest
G
Google Developers Blog
博客园_首页
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
D
DataBreaches.Net
PCI Perspectives
PCI Perspectives
Forbes - Security
Forbes - Security
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Cloudflare Blog
AWS News Blog
AWS News Blog
Latest news
Latest news
T
Tailwind CSS Blog
P
Palo Alto Networks Blog
Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
Cyberwarzone
Cyberwarzone
T
The Exploit Database - CXSecurity.com
WordPress大学
WordPress大学
Recorded Future
Recorded Future
A
Arctic Wolf
V
Vulnerabilities – Threatpost
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
人人都是产品经理
人人都是产品经理
月光博客
月光博客
有赞技术团队
有赞技术团队
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
美团技术团队
Hacker News - Newest:
Hacker News - Newest: "LLM"
Jina AI
Jina AI
N
News and Events Feed by Topic
Attack and Defense Labs
Attack and Defense Labs

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽 片刻 - Jdeal | Life is like a Design.
Just A Common Lisper
Eltrac · 2026-06-02 · via BlogFinder

最近在玩 Common Lisp ,由于中文博客几乎没有讨论,包括文档在内的英文资料都很零散——官网上只有简单的上手介绍,而社区维护的 文档 的某一页竟然写着 Please help us fill this page,我想这是因为 CL 的实现太多了,相关的网站很多,人力是分散的——看来得自力更生了,也借此写一篇博客,既用作信息整理,也试图提高一些 Lisp 的讨论度吧。

尽管 Common Lisp 的具体实现之间差异不会太大,但我想还是说明一下比较好:我使用的 Common Lisp 实现是 Steel Bank Common Lisp ,即 sbcl

文章没有什么主题,可以当作一篇 Lisp 杂谈,还有一些我对编程语言的思考,希望读者可以在文中找到一些有启发的观点和设计吧,不过我不保证什么。

我的启蒙 Lisp 是 Clojure ,这门语言最明显的特征是使用了 ()(Parentheses)以外的其他括号,比如 [](brackets) 和 {}(braces)。其中,[] 表示向量,也被大量地用在函数签名、let 绑定等语法上;{} 表示映射,放在花括号里的奇数个元素是键,偶数个元素是值。这么做的好处是极大地缩短了代码,坏处是…… 不够纯粹?

据我所知,Common Lisp 和 Scheme 都只有一种括号,语法非常简单,所有程序结构都用圆括号包裹,不需要额外记忆别的语法。几乎没有语法也是 Lisp 的特点之一,而 Clojure 的做法无疑引入了不少新语法。

我目前还不能很好地回答这个问题,我只知道,在 Common Lisp 里不能像 Clojure 那样爽写映射,很不习惯。

要在 Clojure 里表示键值对,可以这样写:

{:key1 "val1"
 :key2 "val2"
 :key3 "val3"}

要在 Common Lisp 里表示键值对,需要用到 cons,这是我在 Clojure 里没仔细了解过的概念,不过据说是 Lisp 的基础概念,在包括 Clojure 在内的大多数 Lisp 方言中都有实现。这被称作「列表构造函数」,(cons x y) 的意思是「把 x 加入 y」。cons 的意思应该是 construct(构造)。

两个参数都是原子的 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 Number)和寄存器编号的减量部分的内容(Contents of the Decrement part of the Register Number)。之所以这么命名,貌似是出于一些历史原因,最早的 Lisp 是在上世纪的 IBM 704 计算机上实现的,和硬件强相关。不过具体的背景我就不打算深究了,这不是词源学系列。

当我们嵌套有序对,就构成了列表,例如 (cons 42 (cons 69 (cons 613 nil))) 构造以下的数据结构。最后一个 nil(空值)表示列表结束。

图源: cons - Wikipedia

一般而言,上述结构用 (list 42 69 613) 简单表示。

显然,有序对除了不断嵌套表示列表结构,其本身就可表示一个键值对,即用 cons 表示键和值的关系(association)。把一系列扁平的关系都放进列表里,就成了关系列表(association list),也就是 alist

(defparameter *alist* (list (cons 'a 1)
                            (cons 'b 2)
                            (cons 'c 3)))

这段示例代码来自 Marin Atanasov Nikolov ,其中 'a 表示对符号 a 的「引用」,以 ' 开头的符号不会被求值,而是会原样保留,可以用来表示键。

要获取 alist 当中的某个有序对,可以用 assoc 函数。(assoc 'a *alist*) 会返回 (A . 1)1。如果只想要键或者值,用 cdr 或者 car 就好了。此外,还可以用逆向关联 rassoc(reverse association),通过值来查找有序对,(rassoc 1 *alist*) 也会返回 (A . 1)

1.

Common Lisp 大小写敏感,所有代码最终都会被转换为大写字母。 ↩︎

这个数据结构没有引入新的语言机制,非常简单。请思考一下,如果要给这个映射添加新的关系,应该怎么做?

cons 把新的对加入到原来的列表中就行了,就像这样:(cons (cons 'd 4) *alist*)。还有另一个很好用的函数叫作 acons,它可以从第一和第二个参数创建新的有序对,然后再执行 cons,这样就可以少一层嵌套了:(acons 'd 4 *alist*)。不过这两个函数都不会修改原来的 *alist*,而是返回新的数据结构,编写函数式程序很合适;如果想要改变原来的变量,可以用 push,就像这样:(push (cons 'd 4) *alist*)

此外还有一个和 Clojure 的 zipmap 很像的 pairlis 2,接收两个列表,然后按照他们的索引(index),把他们组合成 alist

2.

我没拼错,就是 pairlis,没有 t ↩︎

(pairlis '(a b) '(1 2))
;; => ((B . 2) (A . 1))

有序对组成的列表看起来很符合直觉,不过从结构上看,((A . 1) (B . 2)) 为什么不能变成扁平的 (A 1 B 2) 呢?我联想到数据库设计的 Schema-On-Write 和 Schema-On-Read,前者坚持写入提前定义好的结构化数据,数据的结构本身就应该表达明确的语义,后者则在读数据时才解析数据结构(比如可以把 JSON 字符串写进数据表里,读取之后再解析 JSON),更加灵活也降低了数据库设计的复杂度。

Clojure 的 {} 语法就是这样的,Common Lisp 也可以做到,可以直接把键值对按照奇偶顺序写进列表里,这种列表叫作属性列表(Property List),即 plist。从结构上看,plist 只是列表,不能表示映射关系,但在读取数据时,可以按照约定俗成的方式解析,此时它就有了映射关系。用 getf 就可以从 plist 中获取键对应的值。

(let ((plist (list :a 1 :b 2)))
  (getf plist :a))
;; => 1

其中 : 开头的值表示关键词(keyword),和 Clojure 等 Lisp 方言一样,类似无需提前定义的枚举类型,一般表示键值对的键。尽管这样一来和 Clojure 的差别就小了些,但还是要写个 list 函数。

这么看来,Clojure 作为一门目前看来比 Common Lisp 抽象层次更高的语言,帮我隐藏了不少底层细节,用起来令人愉悦(比如 Clojure 中 car 对应 firstcdr 对应 rest)。Common Lisp 尽管在大多数时候也不需要接触这些细节,但在学习时会自然地接触到相关概念,体感上是更适合用来研究(满足好奇心)的语言。

Lisp 的基石当然是列表,一般的列表都表示函数调用(至少看起来是这样),第一个列表元素是函数名,之后的元素都是参数。如果不关注语言底下是什么,这么理解 Lisp 代码没有问题,加法 (+ 1 2) 是函数,逻辑运算 (or t nil) 是函数,赋值 (setq *foo* "bar") 也是函数——语法统一,非常美好。

但事实并非如此,不是所有列表都是函数调用,有些东西无法通过函数实现。比方说 if,它接收三个参数:条件、条件为真时求值的表达式和条件为假时求值的表达式。

(if (= a 1)
  (format f "A equals 1")
  (format f "A does not equal 1"))

如果 if 是函数,那么这个函数是没有意义的,因为在调用函数时,程序会先求值所有的参数,然后把求值过后的参数传递给函数体。然而,if 存在的目的不就是不求值某一条分支语句吗?

况且 format 函数的返回值是 nil,上述代码如果是函数,只会得到这样的表达式:(if t nil nil)3——无论 if 函数怎么实现,这种调用都没有意义。

3.

Common Lisp 用 t 表示逻辑真,用 nil 表示空和逻辑假,没有 f 或者 false。 ↩︎

or 也不是函数。或运算只要有一个真值,整个表达式的值都为真,那么把所有参数都求值过后再计算就很低效,在求值到第一个真值时就可以返回 t 了。

这些不是函数的“函数”,要么是宏,要么有着特殊的解释(或编译)规则。

之所以想谈这个话题,是因为我在某个课程设计项目中嵌入了一门脚本语言。什么样的语言最容易编写解释器呢?当然是 Lisp。只需要根据括号和原子进行分词,根据词元构造出抽象语法树,再递归地求值就好了。(其实我也可以把 Lua 之类的轻量脚本语言嵌入进去,但为什么不用 Lisp?)

其中有相当一部分“函数”会直接绑定到某个 Go 语言函数上,另外一些被定义为「特殊形式」,会做特殊处理。不过我在这里做了一个不太明智的决策:由于这是个多维表格应用,所以我内置了一些快速获取表格数据的「特殊形式」,比如 (where this (= id 1)) 能够获取 id1 的行,其中 (= id 1) 创建了新的上下文,id 被绑定到表格的列名上,并且每次遍历都会执行一遍,这显然不是个函数,因为 (= id 1) 若是直接求值就会报错。如果要用函数实现,就要传入一个用于筛选的匿名函数,比如这样 (where this #(= (get % "id") 1))4——这更符合直觉,但语法又变得复杂了。

4.

这里借用了 Clojure 的语法,其中 #() 表示匿名函数,% 指代函数的参数。值得一提的是,Common Lisp 中的 #() 表示数组。 ↩︎

然而编写特例又会让语言本身变得臃肿,所以另一种做法是编写宏(macro),也就是能够写代码的代码。既然 (where this #(= (get % "id") 1)) 可以用函数实现,不必编写特例,而 (where this (= id 1)) 更简洁,那我能不能编写宏,让 (where* this (= id 1))5 在解释(或编译)之前就被展开为 (where this #(= (get % "id") 1)) 呢?

5.

这里之所以写的是 where* 而非 where,是因为宏和函数共享命名空间,不可能有一样的名字。 ↩︎

显然是可以的,或者至少可以做到类似的形式,并且宏定义是可以写在标准库和第三方库里的,这样一来,就无须在解释器(或编译器)中编写特例。

回到 Common Lisp,whenunless 宏会被展开为 (if ... do nil)(if ... nil do),而 Common Lisp(当然 Clojure 和不少 Lisp 方言都有这两个宏)的解释器和编译器不需要单独实现 whenless,只需要实现 if,剩下的只要交给宏就好了。

当然,除了保持语言简洁,宏还能方便程序员拓展语言能力,用自己喜欢的方式编写代码而不必忍受语言限制。

可能是因为我一开始读过的不少 Lisp 相关文章和书籍,都把 Lisp 和函数式编程联系起来,我一开始以为 Lisp 都是纯函数式的(思来想去,应该是鲍勃大叔的《整洁架构之道》把函数式编程范式称作「不少 Lisp 方言实现的范式」,误解就由此产生了),我还因为 Haskell 是纯函数式的编程语言,把它误以为是 Lisp 方言,闹过一些笑话。

Clojure 的确是一门整体上更偏好函数式编程的语言(关于什么是函数式,可以读 这篇文章 ),比较直观的体现是,几乎所有的 Clojure 教程和示例代码都没有定义变量然后再赋值的教程。默认情况下,几乎所有变量都是不可变的,包括用 let 局部绑定的变量。

而 Common Lisp 似乎不强调这点,Common Lisp 可以轻易地给变量赋值,不过这种变量被称作动态变量(dynamic variable)。

(defparameter *dynvar* 1)
(setq *dynvar* 2)
*dynvar*
;; => 2

不过我还是更偏好无状态的程序,就算要用全局动态变量,也应该控制数量。只要不会太繁琐,用参数传递状态就足够了。如果要定义局部变量,就用 let

(let ((a 1)
      (b 2))
  (+ a b))
;; => 3

let 显然不是函数,而是特殊形式,因为它创建了一个变量作用域,还包含未声明的符号,这些符号若是被当作函数参数求值就会报错。不过,它底下到底是怎么实现的呢?和其他语言的 let 变量声明一样吗?

在《Let Over Lambda》一书中,作者这样写道:

Although what let does is unambiguous, how it does it is deliberately left unspecified. What let does is separated from how it does it. Somehow, let needs to provide a data structure for storing pointers to values.

尽管 let 的作用并不模糊,它发挥作用的方式却被刻意地留白了。let 的作用与它的作用方式分离。通过某种方式,let 需要提供用来储存指向值的指针的数据结构。

据后文所言,Common Lisp 编译器会根据情况处理 let,选取最好的「存储」方案。程序员也可以在代码中提供更多的信息,这样 Lisp 编译器就能编译出效率更好的代码。

(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

((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 的立即执行函数。

(function(a, b) {
    return a + b
})(1, 2);
// => 3

这相当于:

((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——用 defgenericdefmethod 实现多态,用 defclass 定义类,用 make-instance 实例化类——尽管函数式和面向对象并不冲突,但这也说明 Common Lisp 不那么注重函数式风格,它更像是一门什么都能干的语言。


后面的知识,之后再来探索吧。