2026-07-16
ECZN 给 SICP 作序
算机程序的构造和解释,全称《Structure and Interpretation of Computer Programs》,简称 SICP,以 Scheme 语言为基础讲解了计算机程序的构造和解释,在很大程度上影响了我对编程语言的理解和认知,本文是我在看完 SICP 并工作了数年之后,出于喜好专门为其作的一篇序,如果能看下来并感到有意思,那么你很可能是这本书最适合阅读的人群;反之如果你认为没意思,此书可能不适合你,作序的目标就在于此:15 分钟看完序来判断这本书适不适合你,值不值得花费几十甚至数百个小时的投入 SICP Wikipedia
编每条指令含义是机械性的,执行后自加 PC 就可以算作微机原理里最关键的 loop 了,但汇编分析经常需要人肉推导寄存器状态,特别是各种 jmp 乱飞和 ret 返回以及 stack 和 void*,往往会很痛苦容易迷失在各种寄存器和硬件/调用栈细节里,比如某些场景为了性能甚至会利用 "溢出" 或者硬件/位操作/编码特性来做优化,比如初始化为 0 很可能不是一个 mov ax 0 而是 xor ax, ax
相比之下 Scheme 就纯粹太多,给我的感觉像是一套完备的形式/逻辑系统,寥寥几行可能蕴含着精妙的逻辑构造,比如下面这个
cons
结构以及两个操作原语:
00(define (cons x y)
01  (lambda (m) (m x y)) )
02
03(define (car z)
04  (z (lambda (p q) p)) )
05
06(define (cdr z) 
07  (z (lambda (p q) q)) )
08; 本文会让你明白这段代码的精妙 ;
汇编的性能魔法确实有独到的价值:但是这不是程序的本质,即使去掉这些优化,代码依然能跑,你是否思考过:一个最小的编程语言到底能有多精简?这是本书最核心的主题:语言最小的构造是怎样的,以及如何解释这个最小的构造,它能达到怎样的表达力?正如书名:计算机程序的构造和解释
汇编 VS 括号树
汇编 VS 括号树
本文目标是让你带你快速领悟到这个最小构造精妙在哪,以及阐释它所蕴含的强大表达力,如果能读懂并接受本文则说明你非常适合阅读此书,达成我作序的目标:告诉读者这本书适不适合你。
首先,你必须克服并接受 Scheme 的这两个核心语法特征,这是理解 “最小的编程语言” 的关键所在:
  1. 1.
    前缀表达式和括号地狱: 用
    (+ 1 1)
    而不是
    1 + 1
    , 用
    (add 1 2)
    而不是
    add(1, 2)
    ,因此括号会很多,但配合 IDE 彩色括号及括号匹配基本没啥问题
  2. 2.
    代换化简而不是赋值: (add 1
    (add 1 1)
    ) → (add 1
    2
    ) → (+ 1 2) → 3
    有且仅有两条代换规则:
    1. i.
      引用代换: 比如有
      (define a 1)
      的话,后续
      (+ a a)
      可以代换为
      (+ 1 1)
    2. ii.
      过程代换: 比如 (define add
      ( lambda (x y) (+ x y))
      ) 的话,后续
      (add 1 2)
      可以代换为
      (+ 1 2)

      特别地、lambda 也可以缩写为 (define (add x y) (+ x y))
文圈,甚至可以说整个业界对玩 Lisp/Scheme 或者说玩 PL[1]Programming Language Theory, 编程语言理论, 简称 PLT 或 PL 的都有一种对精英/geek/做题家/理论家的嘲弄或玩梗,这更多是缺乏一个现代化的教材推广导致的,很多人作序上来就是
元语言编程
函数式 λ 演算
这类很唬人的东西,任何初学者看了一下子就会被分流成两类人:中二病会瞬间来了兴致,普通人只会感到卧槽,而后者在看了第一章那些花里胡哨的括号语法和迷惑的前缀表达式后大概率演变为 "我只记得这语言的括号太逆天了",虽然这确实很劝退,但配合 IDE 彩色括号和配对功能问题也不大。
先叠个甲:学习 SICP 和 Scheme 并不能给你找到工作,对于我而言 Scheme 代表的是一种看待 "代码" 的全新视角,通过学习它可以从更高阶的视角看待编程语言和计算机而不是从 "汇编" 去理解,它更像是给出一种最小的构造去解释什么才是计算机语言(构造),以及 "执行代码" 是什么(解释)
因而我的目的很简单,从一个浅显的视角出发作 "序",方便读者快速把握到这门语言最重要的设计就是括号,正如任何流行过的语言都必然留下真正宝贵的精神财富[2]
语言复杂挑战最佳实践(精神财富)
Scheme最小的编程语言应该是怎样的?精简到少量符号,也能做到超越一切的复杂抽象,代码即数据,语言可自举
JavaScript不同系统间数据交换为何如此痛苦?数据交换就应该是最简单的 []{},XML 真该淘汰了,如果一门语言不支持 JSON 可以笃定它必然会失败
Rust内存安全问题长期困扰业界Rust 所有权系统给出了工业级解答:与 C++ move semantics、Swift ARC 等同时代探索相比,所有权+借用的方案最为干净利落
Go多核时代,并发编程如何变简单?你不该用锁的方式思考并发,而是用通信的方式思考并发: "Don't communicate by sharing memory; share memory by communicating"
一样,Scheme 失去括号就跟 JS 没有 JSON、Rust 没有所有权、Go 没有 goroutine 一样,括号是这本书、lisp 系语言真正的灵魂和品味。
真心强推每一个写代码的,不论是初学的还是资深专家都学一下 Scheme,特别是推荐从 SICP 开始上手,在工作几年后一定会有齁齁齁的回甘[3]举个例子,里面会留习题,而且会给最难的题单独标注星号,意思是不会做也没关系。 其中有一题是:写一个符号求导的函数,比如 x^2 变成 2x,大多数人都能达成 80%+ 的用例通过,能完整通过的并不多我也没做到,但在未来的某一天如果你恰好读了大模型的理论,又恰好看到优化器里的反向传播和梯度求导,你一定会想起来这题的:SICP 特有的把业界最复杂最困难的世界性难题给你简化成一道逐层深入的习题12345,多数人能搞定 1234 并能深刻理解这道题的挑战是什么,但是最后的 4 或者标星的 5 则是地狱级难度的。你在未来很可能又会 callback 想起来这类曾经标星的难题给你的思维挑战确实就是极难的,做不出来可太正常了,但是当时那种深度的沉浸式思考的体验也许会伴随一生,私以为做不出来才是最值得体验的一环,真天才反而不需要此书。,尤其是在这个浮躁的 AI 时代很难有学生的心境去深入思考纯粹的逻辑/数学/形式化问题了(但是话说回来 AI 时代阅读 SICP 应该没有任何门槛性的问题了)
再次叠甲:学习这个并不能给你找到工作或者类似的什么可以量化的名利收益,但如果你希望了解到这类问题的解答时非常推荐你看看:计算机的计算是什么、最小的编程语言是怎样的、写 Rust 的/写 TS 的/或者说 PL 佬们所追求的品味是什么。
先你需要忘记一切,假设你只有一张纸和两条代换规则就行,把代码的执行理解为:依据规则做代换、化简规约并得到结果,类似数学的化简代换过程。我们要忘记汇编的心智模型寄存器和状态 (registers and state),转向拥抱 Scheme 的心智模型符号和代换 (symbol and substitution):
00; 这是一张草稿纸, 用冒号代表注释
01
02(define a 123)
03; 类似数学上经常写的 "令 a = 123"
04; 特别写给 js 程序员: 这等价于 const a = 123
05
06(+ a a)
07; 运算符提前的前缀写法, 等价于 a + a,然后 a 可以代换为 123
08; 所以最终是 (+ 123 123) 得到 246
09
10(define (double x)
11  (+ x x) )
12; 等价于定义一个函数,"令 double(x) = x + x"
13; 特别写给 js 程序员: 这等价于 function double(x) { return x + x }
14
15(double a)
16; 故而上述写法代表 double(a) 也就是 double(123)
17; 最终就是 (+ 123 123) 得到 246
18
19(define double
20  (lambda (x) (+ x x) )
21)
22; double 还可以通过 lambda 的语法糖来表达, 跟前面那种写法几乎意思一样
23; 这里意思是 令 double = (x) => x + x
24; 视角是: 把 (x) = x + x 这件事本身单独拿出来叙述:有一个参数加自己
25; 特别写给 js 程序员: 这等价于 const double = (x) => x + x
26
27; 执行代码就是在做 "代换" 比如 (+ 1 1) 其实就是化简代换成 2
28; 执行 (double a) 就是先找到 double 的定义, 带入得到:
29; 第一步:double 就是 (lambda (x) (+ x x) )
30; 第二步:带入到 (double a)
31; 第三步:得到 ( (lambda (x) (+ x x)) a ) 也就是 "有一个参数加自己,参数是 a"
32; 第四步:代换 a 得到 ( (lambda (x) (+ x x)) 123 )
33; 第五步:把 123 代换到 (x) => x + x 中的 x 最终得到 (+ 123 123) 最后得到结果
34; [备注:有所简化]
先说 cons 这个构造到底妙在哪:它仅仅通过编程语言本身的语法就定义了 "引用/指针" 这个概念,你无需理解任何计算机底层的指针细节就可以理解这个概念:
00(define (cons x y)
01  (lambda (m) (m x y)) )
02
03(define (car z)
04  (z (lambda (p q) p)) )
05; (lambda (p q) p) 代表有两个参数 p q, 代换成 p 返回 (返回左边的)
06
07(define (cdr z)
08  (z (lambda (p q) q)) )
09; (lambda (p q) q) 代表有两个参数 p q, 代换成 q 返回 (返回右边的)
要理解上述过程,关键在于代换,给个例子,你可以定义这种结构,比如一个链表
1 → 2 → 3 → nil
cons
1
cons
2
cons
3
nil
00; [1, [2, [3, nil]]]
01(define _123
02  (cons 1 (cons 2 (cons 3 nil))) )
03
04(car _123)  ; 取出最外层的左边,返回 1
05(cdr _123)  ; 取出最外层的右边,返回 (cons 2 (cons 3 nil))
06            ; 注意上面两步,你一定要拿出纸笔一层层化简才能理解
07            ; 再次强调,你一定要拿出纸笔一层层化简/代换
08            ; 才能知道这里到底在做什么,否则后面文章不用看了
(
  car
  _123
)
car
_123
1
2
3
4
5
6
7
8
第一步:尝试展开代换Step 1 / 8

从 (car _123) 开始,依据前面提到的代换 (Substitution) 化简这个式子。建议你最好纸笔试过、或者跟着这里一步一步推导。

后通过 cons 结构的左/右两边来直接构造 "对象" 了,实际上这就是一个任意的
type Pair<L, R> = [L, R]
00(define (create-user name age)
01  (cons name age))
02
03(define (get-username user)
04  (car user) )
05
06(define (get-userage user)
07  (cdr user) )
你或许会认为这算哪门子对象不就是类数组的
[name: string, age: number]
吗,但这门语言最让人感到精妙的地方在于,它的源码本身就是
cons
—— 这意味着你可以给上面的这种代码风格套一层语法糖,使其写起来更像现代语言。
来,仔细考察一下:
00; 这是 [1, [2, null]]
01(cons 1 (cons 2 nil))
02
03; 那么 (cons 1 2) 可以表达为:
04(cons 'cons (cons 1 (cons 2 nil)))
05; 对应 ['cons', [1, [2, null]]] 遍历这个链表就可以遍历语法树
cons
1
cons
2
cons
3
nil
cons
'cons'
cons
1
cons
2
nil
而这门语言天生的宿命就是:写一个
eval
函数,它通过遍历 cons 语法树,就可以自己遍历自己的源码并把自己代换成其他模样,比如代换成汇编就指编译,甚至还可以凭空造出静态类型系统。
而对于前面那种面向对象的语法,你可以构造和解释如下的这种语法糖来实现这种
Record<string, any>
的对象模式,正如上面的例子一般:
00(struct User 'name 'age)
01; struct 是一个你实现的函数代换
02
03(define eczn (new User 'eczn '17) )
04; new 也是一个你实现的函数代换
05
06(print eczn.name)
07(print eczn.age)
08; 你还需要解释 . 的代换
这些新语法很大程度上被括号和前缀表达统一在一起了,它们跟 define 地位类似了,是关键字;事实上 define 也可以是一个函数代换... 特别你真的在做自举的时候你会发现 define 就是一个代换,甚至当你重新审视 (+ 1 2) 的时: 有没有可能 + 号可以代换删掉? 编程语言甚至可以没有 + 号!??
最后,回到具体的实现上,在这个视角下
(struct User a b c)
不过是一种 cons 构造,你只需遍历它并将其代换为另外一种 cons 构造:
(define (create-user a b c) ... )
、同理
(new User a b c)
也代换为
(create-user a b c)
就行,比如上述的代码经过你的定制化的
myEval
代换后应该产生如下结果(随手写的,不保证对,很久没碰了):
00myEval 处理这段 cons 结构:
01  (struct User 'name 'age)
02  (define eczn (new User 'eczn '17) )
03  (print eczn.name)
04  (print eczn.age)
05
06得到这段最终的 cons 结构:
07  (define (create-User name age)
08    (cons name age) )
09  (define eczn (create-User 'eczn '17) )
10  (print (car eczn)) ; 需要把 .name 代换为 car
11  (print (cdr eczn)) ; 需要把 .age  代换为 cdr
当然,过程中会有相当多有趣(甚至是繁复)的细节,关键在于理解并深刻地看到 —— 任何语言的源码都不是扁平的线性字符串,而是有结构的语法树:以符号代换作为计算机程序的构造和解释,最终产生元语言循环 EVAL-APPLY
这本书还会进一步告诉你:"如何通过操作树的构造和解释来构造和解释这棵树",以及 "最小的语言长什么样",并从中领悟到 PL 理论的品味以及 PL 佬他们到底在追求什么。
推荐必须购买最经典的 Scheme 版的 SICP[4]本人持有三本 SICP:一本用于翻阅、一本用于收藏、一本用于传教/送人,如果你线下认识我可以找我借书看看,很多人推荐在线开源的英文原版,但中文版其实也没那么差,但必须要注意千万不要买这几年新出的 Python / JS 版的,括号才是 SICP 的精髓,只有括号能解答 “计算机程序的构造和解释” 的构造和解释。
如果你能喜欢本文那么整本书几乎必然是你最爱的那类,本文用了跟原书类似的结构和写作手法,祝你阅读愉快。
最后,本序的结构严格按照了 SICP 全书的目录结构做的编排,全书分为如下部分:
原文章节本文结构
第一章:构造过程抽象草稿纸,代换化简
第二 / 第三章:构造数据抽象 / 模块化、对象、状态cons 是什么,以及如何构造对象
第四 / 第五章:元语言编程 / 寄存器机构造和解释编译器魔法,以及面对这门语言的终极宿命:构造和解释自己