第 4 部分 · 命令式与状态
从状态到世界——为什么会有命令式、GUI、Web
写到这里,你已经能用 Racket 算账、处理列表、递归、管状态了。接下来要踏入一片新的地界——界面、对象、Web。在动手之前,先退一步看清:这些东西为什么存在?
你大概有过这样的困惑:编程里为什么有那么多“领域”?命令式、面向对象、GUI、Web……它们听起来像一门门独立的手艺,让人还没学就先发怵。其实它们不是各自孤立的发明,而是同一条线上的不同刻度——状态越来越复杂,管理状态的手段就得跟着升级。
这一章不教你任何新语法。它只做一件事:把卷四的命令式、卷五的对象与界面、卷七的 Web 串成一条线,让你看懂每一个“领域”到底在解决什么问题。看清这条线,后面学什么都水到渠成。
先回到最纯粹的计算
卷三我们学的是函数式——输入一个值,经过变换,输出一个值,中间不改任何东西。这是最干净的计算:
(define (square x) (* x x))
(square 5) ; 25
你给它什么,它就老老实实算什么。同样的输入永远得到同样的输出,没有意外、没有记忆、没有时间流逝。这种纯粹有它的代价——有些问题用纯函数写起来很绕——但它有一个无可替代的好处:好懂、好测、好组合。 因为没有任何隐藏的状态在背后捣鬼,你看一眼函数就知道它会干什么。
函数式是计算最原初的样子。问题在于,现实世界不是这么运转的。
现实世界会变:命令式登场
你写一个红绿灯模拟,灯不能是“永远的红”——它得随时间从红变绿、变黄、再变回红。这种“会随时间改变的东西”,就是状态。
卷四的前几章讲的就是怎么管这种状态。设立一个会变的量,用 set! 改它,用条件判断决定什么时候改、改成什么:
(define color "red")
(define (next)
(set! color
(cond
[(equal? color "red") "green"]
[(equal? color "green") "yellow"]
[(equal? color "yellow") "red"])))
这就是命令式的核心:状态会随时间改变,你的程序负责一步步地驱动这个改变。 红绿灯状态机就是它的典型形态——一组状态、一套转移规则、一个驱动它们运转的循环。
到这里你管的是一两个零散的状态。够用,但有限。
状态多到管不过来:对象登场
假设你写一个桌球游戏。台面上有十几个球,每个球都有自己的位置、速度、颜色,而且都在动、都会碰撞。如果还用命令式那套——一堆零散的变量、一堆 set!——代码很快会乱成一团:哪个变量属于哪个球?碰撞时谁改谁?十个球的状态纠缠在一起,改一个就怕动了别的。
问题的本质是:状态变多了,而且它们是成组的——一个球的几个状态必须绑在一起,作为一个整体被看待、被修改。
面向对象就是为解决这个问题而生的。它让你把“数据和行为”打包成一个独立的单位——对象。每个球是一个对象,自己记着自己的位置和速度,自己知道怎么移动、怎么响应碰撞:
(define ball%
(class object%
(super-new)
(define x 0)
(define y 0)
(define/public (move dx dy)
(set! x (+ x dx))
(set! y (+ y dy)))
(define/public (where) (list x y))))
你看,状态(x y)和行为(move where)被关进了同一个盒子里,外面只能通过对象提供的方法来碰它们。对象就是带数据和行为的小盒子,类就是造这种盒子的图纸。 这句话是面向对象的全部起点。
十几个球就是十几个独立的对象,各管各的,互不干扰。复杂度没有消失,但被切碎了——从“一堆纠缠的变量”变成“一组各司其职的对象”。这就是面向对象的真正价值,它不是什么高深的哲学,就是一个管理复杂状态的实用办法。
状态要给人看:界面登场
对象一旦有了,很自然地,你会想让它们“可见”——让用户能看见、能操作。一个桌球游戏,不能只让球在内存里跑,得画到屏幕上、能用鼠标点。
这就是 GUI(图形用户界面)的由来。但你可能没想过,GUI 其实有一个历史脉络——它不是某天突然发明的。
最早的计算机没有屏幕,人和机器交流靠打孔卡片和打印纸。后来有了命令行——你在键盘上敲一行指令,机器打印一行结果,一来一回。这用了几十年,至今程序员还在用,因为它高效、直接。
但命令行有个门槛:你得记住指令、得会打字、得看得懂文字输出。1960 年代后期,道格拉斯·恩格尔巴特做了一场被称为“所有演示之母”的演示,第一次展示了鼠标、窗口、超文本。
接着施乐公司的 PARC 研究中心把这些想法做成了真正能用的图形界面系统 Alto。1984 年,苹果的 Macintosh 把图形界面带进了大众市场。再后来,浏览器出现了,把图形界面搬到了网络上——这就是 Web。
这条“命令行 → 桌面 GUI → Web”的演变线,背后是同一个趋势:让计算的结果从“只有程序员看得懂的文字”,变成“任何人都能看懂、能操作的图形”。
技术上也跟着变了。GUI 框架无一例外都建立在“控件 + 事件”这个模型上:屏幕上摆着一堆看得见的控件(按钮、文本框、滑块),用户点什么、拖什么,就产生一个事件,程序响应这个事件去改变状态、重绘画面。这不是巧合——“看得见的控件接收用户操作”,是图形交互最自然的抽象。
所以你接下来在卷五会写的 Racket GUI 代码,本质上是这样一个循环:摆控件 → 接事件 → 改状态 → 重画。理解了这个循环,你就理解了所有 GUI 框架的内核,不管是 Racket 的 racket/gui、Java 的 Swing,还是网页上的 HTML + JavaScript。
没有记忆的协议:Web 登场
Web 是这条线上最特别的一个。它解决的不是“状态太多”或“状态要给人看”,而是一个更刁钻的问题:怎么在一个天生没有记忆的协议上,造出“有记忆”的体验?
HTTP 协议是无状态的——服务器处理完你的请求就忘了你是谁,下一个请求它把你当成全新的人。这本来是个优点:服务器简单、能扛海量请求。但它给写应用的人出了个难题:用户登录了一次,凭什么下一次请求服务器还认得他?用户往购物车放了个东西,刷新一下凭什么还在?
Web 开发的很大一部分,都在跟“怎么在这个健忘的协议上维持会话”较劲。办法有 cookie、有 session、有数据库存状态。
而 Racket 的 Web 框架有一个独门绝技——用延续(continuation)来管会话。延续是卷六会讲的 Racket 的杀手级特性,它能让你写出“像在跟用户进行一次连续对话”的代码,而不必手动把每一步的状态塞进数据库再取出来。
所以 Web 不是凭空多出来的一个领域,它是状态复杂度的又一个刻度:当状态要跨过一个“健忘的网络”延续下去时,你需要新的手段。
一条线,四种刻度
把上面这些连起来,你会看到一条清晰的递进:
| 刻度 | 要管的状态 | 典型手段 | 这本书的卷 |
|---|---|---|---|
| 纯计算 | 没有 | 函数变换 | 卷三 |
| 命令式 | 几个会变的量 | set! + 循环 + 条件 | 卷四 |
| 对象与界面 | 成组的状态、要给人看 | 类 + 控件 + 事件 | 卷五 |
| Web | 跨健忘网络延续的会话 | HTTP + 延续 + 数据库 | 卷七 |
每一刻度都不是新发明,而是上一刻度处理不过来时的升级。内核始终是那个“用符号和规则描述计算”,只是状态的规模和形态变了,管理的手段就得跟着变。
[架构图:状态复杂性的四级阶梯——每级是上一级的升级]
理解了这条线,你就能看穿那些唬人的名词。“面向对象”不是什么革命,就是管复杂状态的打包术。“GUI 编程”不是新物种,就是让对象可见、可点。“Web 开发”不是另一个世界,就是跟无状态协议较劲。它们的根,都在你已经学会的那些东西里。
下一篇,我们就正式动手——用 Racket 的类系统,写出你的第一个对象。