第 7 部分 · 走向生产:工程化与实战
读懂 Racket 官方文档——Guide、Reference 与 contract
Racket 文档读不懂,多半是入口选错了:Guide 讲思路,Reference 定规格,那一串方括号和箭头是 contract。你照着教程敲过几段代码,可曾真正打开过 Reference?
你照着教程敲了几段 Racket,想自己写点什么。打开官方文档,满屏的方括号、冒号、->、or/c,像另一种语言。搜一个 map 的用法,跳进一个布满 BNF 产式的页面,比函数本身还难懂。
这不是英语不够,是文档的定位和你的预期错位了。Racket 官方文档不是教程,是规格说明书。 书籍教你思考,文档教你实践。 划清这条界线,文档就从一堵墙变成一把工具。
Guide 是教程,Reference 是规格
docs.racket-lang.org 上最常被打开的两本,是《The Racket Guide》和《The Racket Reference》。它俩不是同一件事的两种写法,分工完全不同。
《The Racket Guide》是教程。它按概念推进,配大量可运行的示例,告诉你为什么这么设计。想搞懂模块、宏、continuation 怎么回事,读 Guide。它默认你顺着读,像一本会说话的书。
《The Racket Reference》是规格。它逐条列出每个绑定——一行签名、一段契约、几行文字、几个例子,没有半句多余。同一个 map,Guide 花一页讲它和 for/list 的取舍,Reference 只给你精确签名和边界行为。它假设你已知道要找什么,来这里只是核对。
旁边还挂着一张 Racket Cheat Sheet,一页纸的速查表,忘了函数名去翻它最快。
学概念读 Guide,查用法查边界读 Reference。
拆开一份签名
挑一个看着吓人的——call-with-output-file。Reference 里它的条目长这样:
(call-with-output-file path
proc
[#:mode mode-flag
#:exists exists-flag]) → any
path : path-string?
proc : (output-port? . -> . any)
mode-flag : (or/c 'binary 'text) = 'binary
exists-flag : (or/c 'error 'replace 'truncate ...) = 'error
exists-flag 实际能取 append、update、truncate/replace 等多个值,完整签名还多一个 #:permissions 关键字;这里省掉是为了看清结构。五个符号认全,这页就读通了:
- 方括号
[]:可选。path、proc没套方括号,必填;两个#:套在方括号里,可选。 - 关键字
#::按名字给参数。调用时写#:mode 'text,顺序随意、自带说明。 - 冒号
::后面是类型约束。path : path-string?意思是path必须满足path-string?。 - 等号
=:默认值。(or/c 'binary 'text) = 'binary表示不给就当'binary。 - 箭头
→:返回类型。这里返回any,意味着返回什么由你传进去的proc决定。
最朴素的调用只要必填的两项:
(call-with-output-file "out.txt"
(λ (out) (display "hi" out))) ; out 是 output-port,返回 void
contract:箭头两边的类型
签名里冒号后面那些东西,是 Racket 的契约(contract)。path-string?、procedure? 这种带问号的,是普通谓词;or/c、-> 这些是契约组合器,把简单谓词拼成复杂约束。
最难啃的一行是 proc : (output-port? . -> . any)。拆开看:proc 必须是个函数,它吃一个 output-port?,吐一个 any。中间那对点是 Racket 的中缀写法,(a . -> . b) 读进去就是 (-> a b)——纯粹为了写着像数学里的 A → B。
顺手把 any 说清楚:它表示不对返回值做检查,只能写在箭头右边,函数返回几个值都行;any/c 则是“任意单个值”,到处都能用。call-with-output-file 把 proc 写成 (output-port? . -> . any),自己又返回 any,意思是 proc 吐出什么,它就原样回传什么。
常用的契约组合器就那么几个:
(or/c number? string?) ; 数字或字符串,二选一
(and/c number? positive?) ; 同时是数字且为正
(listof string?) ; 每个元素都是字符串的列表
(cons/c symbol? number?) ; car 是 symbol、cdr 是 number 的 pair
(-> number? number? number?) ; 吃两个数、还一个数的函数
带可选参数或关键字参数的函数,用 ->*,三组括号分别是必填、可选、返回:
(->* (number?) ; 必填:一个数
(string?) ; 可选:一个字符串
boolean?) ; 返回:布尔值
contract 是运行时检查的契约,不是静态类型。 默认的 Racket 没有编译期类型系统,这些约束只在程序跑到那一步时才生效,违反了就抛 contract violation。想知道一个值要满足什么,看契约;想要编译期保证,那是 Typed Racket 的地盘。
把文档拉到眼前
raco docs 是把文档拽到眼前的最快路径。命令行敲:
raco docs map
它打开浏览器,在所有已安装的文档里——核心文档加上你装的包——搜这个词。函数名、模块名、甚至报错信息里的关键词,都能这么查;搜不到就换英文、换同义词,或者退回 Guide 目录按章节找。
在 DrRacket 里更快:光标停在某个标识符上按 F1,跳到它的文档搜索结果;按 F2 直接弹一个签名框,连页都不用跳。看到不熟的函数,F1 比搜索引擎快。
网页端 docs.racket-lang.org 右上角有搜索框,能搜的东西比你想的多:
- 函数名:
string-split - 概念名:
pattern matching - 符号本身:
#:keyword
从示例反查
Reference 里几乎每个函数都附了示例,长这样:
> (map add1 '(1 2 3))
'(2 3 4)
> 开头是你在 REPL 里敲的,下一行是求值结果,复制进 DrRacket 或 REPL 就能跑。
签名读不懂时,示例永远先开口。 一段 (map add1 '(1 2 3)) 比盯着 proc : procedure? 想五分钟更直白。先扫示例看懂行为,再回头对签名抠细节。
报错信息也指向文档。撞上一个 contract violation:
; map: contract violation
; expected: procedure?
; given: 5
它把期望的契约(procedure?)和实际给的值(5)都写清楚了。拿 procedure? 或 map 回文档一查,就知道第一个参数必须是函数,而你传了数字。
每个函数条目末尾还会列相关函数。查 map 顺带看到 for-each、andmap、ormap;查 filter 看到 partition、remove。一次查询,顺藤摸瓜。
Guide 教你怎么想,Reference 告诉你到底是什么。两者在手,Racket 整座生态就向你打开了。
到这里,卷一到卷六的旅程走完了——你从“编程是什么”出发,经过了函数式、命令式、面向对象、GUI、宏、延续,直到亲手造一门语言。你已经理解了 Racket,也理解了编程语言为什么是这个样子。
接下来的卷七,我们换一个节奏:从“理解语言”转向“用语言做产品”。下一篇带你认识 raco——Racket 工程体系的控制中心。从它开始,你会逐步掌握把代码变成能上线产品的一整套工具链。