Racket:不只是语言,更是语言平台
Racket 不只是编程语言,更是一个用来设计语言的平台。
几乎所有编程语言都有一个共同的前提:你得接受语言设计者给你的语法和语义。想用 unless 替代 if-not?大部分语言只能说"不支持"。想为你的业务领域发明一套专用语法?要么写一个完整的编译器,要么忍着。
Racket 的答案是:语言本身就应该是一个创造语言的工具。
从教学工具到语言平台
Racket 的前身是 1995 年启动的 PLT Scheme,最初只是为计算机科学教学提供编程环境。团队发现 Scheme 虽然简洁优雅,但无法在一小时内教会学生并让他们专注于计算本质,于是创造了一门专门为教学设计的语言和配套的编程环境。
到 2010 年,这门语言已经发展得面目全非。PLT Scheme 正式更名为 Racket(发布 5.0 版本)。Mat Felleisen 等人解释过,"Scheme" 这个标签让人误以为这只是极简的 S-expression 方言,而 PLT 团队的目标一直是构建功能完备的语言平台。更名明确了 Racket 的身份:Lisp 家族的后代,同时也是一个能够承载多种语言的独立平台。
#lang:一行代码切换语言
Racket 语言扩展能力最直观的体现是 #lang 指令。在源文件顶部写一行 #lang racket,就声明了使用核心 Racket 语言;换成 #lang typed/racket 则使用静态类型方言;#lang scribble/manual 用于编写文档;#lang htdp/bsl 是面向初学者的简化语言。
这些方言并非简单的库,而是拥有各自语法和语义的独立语言,但它们共享同一套 Racket 运行时、编译器和工具链。Scribble 文档语言就是一个典型例子——它的代码看起来跟传统的 Lisp 毫无关系,但它确实是运行在 Racket 平台上的语言。Racket 的官方文档就是用 Scribble 编写的。
这也是 Racket 与传统 Scheme 最关键的分界线。Scheme 程序通常不使用 #lang 前缀,Racket 程序默认也不符合 Scheme 标准,两者互不兼容。选择 Racket 还是 Scheme,实际上是在功能完备性与标准兼容性之间做取舍。
宏系统:从"文本替换"到"语法编程"
传统 Lisp 宏直接操作原始 S-expression,说白了就是在操作文本。Racket 的宏则以 " 语法对象 "(syntax objects)为基础——语法对象不仅包含代码本身,还携带源代码位置、词法上下文和变量绑定信息。正是这些额外信息使得 Racket 的宏具有 " 卫生性 "(hygienic):当宏展开生成临时变量时,宏展开器会自动重命名以避免与用户代码中的同名变量冲突。
这个机制把宏从一个容易出错的工具,变成了可靠、可组合的构建块。宏展开器在程序编译或求值之前递归遍历语法树,用宏生成的新代码替换宏调用。这个过程本质上把宏系统变成了一个编译器扩展 API,允许程序员添加新语言特性乃至全新的DSL,使用体验与内置语法无异。
S-表达式 的同像性(homoiconicity)是这一切的基础:代码即数据,程序和数据共享同一种结构,使得编写解释器、编译器、结构化编辑器等元编程工具变得自然。但 Racket 并不局限于 S-expression——借助 #lang 和自定义 reader,开发者可以定义完全不同的表面语法。
多范式:不挑食的实用主义
Racket 摆脱了 Scheme 的极简主义,但它并没有抛弃函数式编程的根基。词法闭包、尾调用优化、delimited continuations、不可变的配对和列表,这些都是内置支持的。同时,Racket 也支持 set! 等命令式赋值,内置基于 mixin 的类系统用于面向对象编程,GUI 库中 new frame% 这样的用法就是面向对象特性的体现。
类型系统 方面,Racket 的主语言采用动态强类型系统,在此基础上提供了独立的方言 Typed Racket——一种渐进式静态类型语言,可与无类型代码混合使用。Typed Racket 是通过宏系统实现的,而不是简单地在现有语言上叠加类型注解,这使其成为类型系统研究的实验场。
至于逻辑编程,社区提供了 Racklog、miniKanren、Datalog 等扩展。这些更多是社区驱动的库和包,不像 Typed Racket 那样处于核心地位,但它们展示了 Racket 扩展机制的灵活性。
模块与契约
Racket 鼓励使用模块系统组织大型程序。其模块系统采用统一设计,提供清晰的编译时检查和顶层定义不可变性。相比之下,Common Lisp 的包系统、ASDF 与编译期机制是由多个历史组件共同构成的,集成度不如 Racket。
Racket 的另一重要贡献是将高阶契约(higher-order contracts)系统化地引入函数式语言。Eiffel 的 Design by Contract 更早,但 Racket 将契约扩展至一等函数、对象和引用单元格等高阶值,推动了后续契约编程研究的发展。契约系统允许在模块边界对函数行为施加形式化约束,与模块系统配合,在保持动态语言灵活性的同时提供更强的正确性保证。
Racket CS:换了一颗心脏
Racket 近年来最重要的工程演进是运行时架构的迁移。自 7.x 版本起,Racket 开始将底层实现从旧的 Racket BC(基于 C 的运行时)迁移到 Racket CS(基于 Chez Scheme),并在 8.x 系列中将 Racket CS 设为默认。这一迁移显著提升了运行时性能和垃圾回收效率,同时降低了代码维护成本。Chez Scheme 本身就是一个以高性能著称的 Scheme 实现,Racket CS 借助其 JIT 编译和优化能力,在数值计算等场景中相比 CPython 有明显性能优势——但具体差距高度依赖工作负载,不能一概而论。
Common Lisp 的对比
把 Racket 和 Common Lisp 放在一起比较是有意义的,因为两者都是 Lisp 家族中功能完备的"大"语言,但设计侧重截然不同。
Racket 的宏系统默认采用卫生宏(hygienic macros),自动避免名称冲突。Common Lisp 则选择了非卫生宏(unhygienic macros),赋予程序员更大的控制权,开发者需借助 gensym 等机制自行处理名称捕获问题——这种设计在成熟的 CL 社区中运作良好,不能简单地说"不安全"。
CL 真正的杀手锏在于极致的交互式开发体验:开发者可以在程序运行时修改代码、重新加载模块、从错误堆栈中恢复。其 CLOS(面向对象系统)和条件系统(condition system)也是业界最为成熟的设计之一。Racket 在这方面做出了权衡,选择以编译时检查和不变性换取更高的健壮性和可预测性。这个取舍没有对错,取决于你更看重哪种开发体验。
Python 的对比
Racket 和 Python 都被用于教学和通用脚本,但实用性差异显著。性能方面,Racket CS 在某些数值计算基准测试中可能显著优于 CPython,但具体优势高度依赖工作负载;在科学计算领域,Python 通常依赖 NumPy 等原生扩展获得更高性能。生态系统方面,Python 拥有庞大且成熟的社区、海量的库和工具,这是 Racket 无法比拟的。教育接受度方面,某些教学研究中的调查显示学生毕业后继续使用 Racket 的比例较低,但这一结果受到课程设置和样本范围的影响,不能简单推广为普遍结论。
生态系统
DrRacket 是 Racket 的旗舰 IDE,由 Racket 本身编写,专为教学和语言设计场景打造。其核心特性包括错误高亮、交互式界面、宏步进器(macro stepper),以及对自定义语言方言的无缝支持——当新方言基于宏系统构建时,DrRacket 能提供与内置语言同等水平的调试和导航功能。
命令行方面,racket 是核心的解释器、编译器和运行时系统,raco 是多功能命令行工具,负责包管理(raco pkg)、创建可执行文件、构建文档等任务。raco pkg 的使用体验类似 Python 的 pip,任何 Git URL 都可作为包源。
标准库覆盖了 Web 开发(web-server)、GUI(racket/gui)、科学计算(racket/math、plot)、FFI(ffi/unsafe)等常见场景。事件空间(eventspaces)和管理员(custodians)等系统级原语使其可以像操作系统一样管理子程序的资源生命周期。实验性项目如 RacketScript 正在探索将 Racket 编译为 JavaScript(ES6),以实现与 Web 生态的互操作。
实际应用
Racket 在教学领域的地位主要来自 HtDP(How to Design Programs)课程体系。该体系通过一系列简化的教学语言(#lang htdp/bsl 等)循序渐进地引入编程概念,DrRacket 的交互式界面和定制化错误报告为此提供了良好的工具支持。美国西北大学、东北大学等高校曾采用 Racket 进行计算机科学入门课程教学。
作为专为语言设计而生的平台,Racket 也是编程语言研究的常用工具。研究人员利用其宏系统和元编程能力,快速原型化新的 DSL、实验新语言特性、研究编译器行为。
商业领域的公开案例相对有限。Matthew Butterick 的排版工具 Pollen(用于出版《Practical Typography》一书)是一个知名案例。社区资料中也列出过一些曾使用 Racket 的组织和项目,但具体情况可能随时间变化。
挑战与展望
Racket 的主要瓶颈在于社区规模和生态系统的有限性。第三方库数量少、维护集中度高("负面巴士系数"问题),外部工具(如在线自动评分系统)也相对缺乏。这些因素形成了一个正反馈困境:小众导致资源不足,资源不足又限制了吸引力。
但换个角度想:如果编程语言本身变成了你可以塑造的材料,而不只是你必须适应的工具,你会设计一门什么样的语言?