The Racket 编程语言 之所以有趣,并非因为它的 syntax。What 使其有趣的是 that 它被设计用来 for 构建语言,而不仅仅是编写程序。
This 文章跳过了 “hello world” 和像猜数字这样的玩具练习(exercises),直接切入 the 使 Racket 与众不同的理念,再加上一些有用的系统脚本编写。
本文范围: This 不是参考手册。它涵盖了一套精选的核心概念,例如 expressions,一等函数,map/filter/fold,模式匹配(pattern matching),宏,契约,以及一些 Linux 脚本编写(Linux scripting)。如需更多细节,请阅读 the Racket 指南 和 the Racket 参考手册。
关于 require 的说明: 下面的一个示例展示了 (require racket/contract)。如果你的文件以 #lang racket(而非较小的 #lang racket/base)开头,则它已被包含,显式的 require 是多余的。不过很多人为了明确文件依赖关系,还是会保留它。
设置 Racket 编程环境
你不需要任何花哨的东西,只需要 Racket 本身。
1. 安装 Racket 语言
从 download.racket-lang.org 下载适用于你平台的安装程序(当前稳定版是 v9.2)。在 Linux 上,你也可以通过包管理器安装,不过官方安装程序通常能让你获得更新的版本:
我们发布了一篇专门的指南,介绍了在 Linux 中安装 Racket 的不同方法。请查看以下链接:
安装完成后,验证 Racket 版本:
racket --version
2. 运行下面代码示例的两种方式
方式 A:作为脚本文件(本文大多数示例采用此方式):
将代码片段保存到一个文件中,文件以示例中显示的 #lang racket 行开头(有些片段是片段,需要粘贴到现有文件中,如果缺少 #lang racket,请自行将其添加为第一行):
#lang racket
(displayln (+ 2 3))
然后在终端中运行它:
racket my-file.rkt
方式 B:在 REPL 中(尝试单个表达式最快的方式):
racket
这会进入一个交互式提示符(>),你可以在其中粘贴本文中的任何单个表达式,并立即看到它的 result:
> (map add1 '(1 2 3))
'(2 3 4)
输入 (exit) 或按 Ctrl+D 退出 REPL。
3. 可选:DrRacket(捆绑的 IDE)
安装程序还包含了 DrRacket,一个带有内置“运行”按钮和代码下方交互面板的图形化编辑器。在处理本文示例(尤其是多行示例)时,这通常是最简单的实验方式。只需打开它,粘贴一个以 #lang racket 开头的代码片段,然后点击“运行”。
让我们 begin。
1. 代码即数据
与 C 或 Python 不同,Racket 源代码本身就是一个数据结构,具体来说,是嵌套的列表。例如,(+ 2 3)
(+ 2 3)
实际上就是一个列表,其第一个元素是符号 +(它指向一个函数),其余的是参数:+、2、3。
因为代码就是数据,你可以通过编程方式构建它并 eval 它。注意,这个示例需要在 REPL 中运行,而不是作为脚本文件。eval 使用当前命名空间,而使用 #lang racket 运行的脚本从一个空命名空间开始,所以这里会引发一个未绑定标识符的 error,而不是返回 30。
(define expr '(+ 10 20))
(eval expr)
示例输出:
30
如果你更愿意将其作为脚本运行,而不是在 REPL 中,请为 eval 提供一个显式的命名空间:
#lang racket
(define expr '(+ 10 20))
(eval expr (make-base-namespace))
将上述代码复制到一个文件中,例如 my-file.rkt,然后使用命令运行它:
racket my-file.rkt
这将打印 30。
这个单一的理念,即代码即数据,是宏得以实现的基础。
你也可以从 DrRacket GUI 运行代码。
2. 一切都是表达式
与 C、C++ 或 Python 不同,几乎每个 Racket 形式都返回一个值。即使对于 if 也是如此。
# Python
if x > 10:
y = 5
else:
y = 20
变成
(define y (if (> x 10) 5 20))
同样地,begin 按顺序运行一系列表达式,并返回最后一个表达式的值。
(define result
(begin
(displayln "Calculating...")
(+ 4 7)))
示例输出:
Calculating...
现在 result 是 11。因为每个表达式都返回一个值,你可以直接组合表达式,而无需为了在某个步骤中保存一个值而创建额外的变量。
3. 函数是一等值
函数可以像其他任何值一样被存储、传递和返回。
(define (square x) (* x x))
(define f square)
(f 8)
64
匿名函数的工作方式相同:
(map (lambda (x) (* x x)) '(1 2 3 4 5))
示例输出:
'(1 4 9 16 25)
你甚至可以将函数放入列表中:
(list add1 sub1 sqrt)
'(#<procedure:add1> #<procedure:sub1> #<procedure:sqrt>)
注意: 每个过程显示的确切文本可能因你的 Racket 版本而异,但它总会显示该列表包含的是函数,而不是数字或文本。
4. 高阶编程:map、filter、fold
与其编写显式的 loop,不如从对集合进行转换的角度来思考。这三个操作可以解决大量问题。
Map - 转换每个元素:
(map string-length '("Linux" "Kernel" "Racket"))
示例输出:
'(5 6 6)
Filter - 保留匹配谓词的元素:
(filter even? '(1 2 3 4 5 6 7 8))
示例输出:
'(2 4 6 8)
Fold - 将元素合并为单个 result:
(foldl + 0 '(1 2 3 4 5)) ; sum -> 15
(foldl * 1 '(1 2 3 4)) ; product -> 24
(foldl max 0 '(2 8 5 12 4)) ; maximum -> 12
5. 递归与尾调用
Racket 确实有 for 和 do 这样的 loop 形式,但许多惯用的 Racket 程序使用 递归 而不是 loop,尤其是在处理列表时。
(define (factorial n)
(if (zero? n)
1
(* n (factorial (sub1 n)))))
(factorial 5)
示例输出:
120
Racket 还保证了正确的尾调用。这意味着,当一个函数在最后一步调用自身时,Racket 会将该调用转换为一个 loop,而不是在调用栈上添加新的一层。因此,下面的函数可以安全地处理数百万个元素而不会耗尽内存。
(define (sum lst)
(define (loop lst acc)
(if (empty? lst)
acc
(loop (rest lst) (+ acc (first lst)))))
(loop lst 0))
6. 命名 Let(读起来像循环的递归)
let loop let 你编写递归迭代,而无需一个单独的顶层函数:
(let loop ([i 0])
(when (< i 5)
(displayln i)
(loop (add1 i))))
示例输出(每次 displayln 调用打印一行):
0
1
2
3
4
看起来像一个 loop,但本质上它是递归。因为对 loop 的调用处于尾位置,Racket 会自动将其转换为一个真正的 loop。
7. 模式匹配
模式匹配是 Racket 最好的特性之一。它来自 racket/match,当你使用 #lang racket 时,它已经被包含在内,所以你不需要添加额外的 require 行。
简单的解构:
(match '(1 2 3)
[(list a b c) (+ a b c)])
示例输出:
6
嵌套结构:
(match '((1 2) (3 4))
[(list (list a b) (list c d)) (+ a b c d)])
示例输出:
10
匹配“命令”形状。这对于解析配置文件或命令行参数非常有用:
(match '(cp file1 file2)
[(list 'cp src dst) (printf "Copy ~a -> ~a\n" src dst)]
[_ "Unknown"])
示例输出:
Copy file1 -> file2
8. 多返回值
Racket 原生支持一个函数返回多个值,而无需将它们打包成元组或列表。
(define (divide x y)
(values (quotient x y) (remainder x y)))
(let-values ([(q r) (divide 20 3)])
(displayln q)
(displayln r))
示例输出:
6
2
9. 列表推导式
Python 的列表推导式看起来像 [x*x for x in nums],其中 nums 是某个数字列表。Racket 的等价物是 for/list:
(for/list ([x '(1 2 3 4)])
(* x x))
示例输出:
'(1 4 9 16)
你可以使用 #:when 添加一个 filter 子句:
(for/list ([i (in-range 20)] #:when (even? i))
(* i i))
示例输出:
'(0 4 16 36 64 100 144 196 256 324)
10. 哈希表
(define h (hash "cpu" 16 "ram" 32))
(hash-ref h "cpu")
示例输出:
16
重要提示:(hash ...) 创建一个不可变的哈希表。hash-set 不会原地更改 h。相反,它会返回一个全新的哈希表:
(define h2 (hash-set h "disk" 1000))
h 保持不变。h2 是新表。如果你想原地更改哈希表,请改用 make-hash 和 hash-set!:
(define mh (make-hash))
(hash-set! mh "disk" 1000)
11. 结构体
结构体是一种轻量级、默认不可变的记录。它是 Racket 对简单类的回答。
(struct person (name age))
(define p (person "Senthil" 40))
(person-name p) ; "Senthil"
(person-age p) ; 40
12. 契约
契约让你在运行时记录并强制执行函数的期望。它们是独立静态类型系统的实用替代方案,尤其是在模块边界处。
(require racket/contract)
(define/contract (divide a b)
(-> number? (and/c number? (not/c zero?)) number?)
(/ a b))
调用 (divide 5 0) 会在除法发生之前引发一个契约违规。错误消息清楚地显示了哪个参数违反了哪条规则。
示例输出:
divide: contract violation
expected: (not/c zero?)
given: 0
in: an and/c case of
the 2nd argument of
(->
number?
(and/c number? (not/c zero?))
number?)
contract from: (function divide)
blaming: top-level
(assuming the contract is correct)
at: string:1:18
[,bt for context]
13. 宏:Racket 真正的超能力
普通函数在运行之前会评估其参数。
(define (twice x) (+ x x))
宏则不同。它在代码被评估之前转换源代码。参数表达式永远不会被预先求值。
(define-syntax-rule (unless-again test body)
(if (not test) body (void)))
(unless-again (> 2 5) (displayln "Hello"))
示例输出:
Hello
这是另一个经典示例。它运行一个表达式两次,并且该表达式在宏展开之前保持未评估状态。
(define-syntax-rule (twice-run expr)
(begin expr expr))
(twice-run (displayln "Hello"))
示例输出:
Hello
Hello
这就是 Racket 在编程语言圈子中闻名遐迩的原因。宏让你扩展语言本身,而不仅仅是调用用该语言编写的函数。
14. 构建 DSL
想象一个看起来像这样的配置文件:
(server
port 8080
ssl yes
workers 8)
这种语法在 Racket 中默认不存在。你可以自己构建它,使用 define-syntax-rule,或者更通用的 syntax-rules 和 syntax-parse 形式。这是一个小例子:
(define-syntax-rule (repeat n body)
(for ([i n]) body))
(repeat 5 (displayln "Ostechnix"))
示例输出:
Ostechnix
Ostechnix
Ostechnix
Ostechnix
Ostechnix
这读起来像是一个内置的语言特性,因为在 Racket 中,你确实可以添加新的语言特性。很少有主流语言能像这样轻松或令人愉快地做到这一点。
15. 惰性求值与流
Racket 默认是急切的(也称为严格)。这意味着它通常立即计算值。但 Racket 也通过 racket/stream 内置了对惰性的支持。
(require racket/stream)
(define nums (stream 1 2 3 4 5))
(stream-first nums)
示例输出:
1
流可以是无限的,因为它们的元素只有在你实际请求时才会被计算:
(define ones (stream-cons 1 ones))
(stream-ref ones 1000000)
示例输出:
1
只有你实际请求的那部分流才会被计算。
16. Linux 上的系统脚本编写
Racket 也可以作为一种非常强大的 shell 脚本和文本处理语言。
读取文件:
(file->string "/proc/cpuinfo")
或逐行读取:
(with-input-from-file "/proc/meminfo"
(lambda ()
(displayln (read-line))))
运行 shell 命令:
(system "ls -lh")
捕获输出而不是打印它:
(with-output-to-string
(lambda () (system "uname -r")))
一个小的 /proc/meminfo 过滤器:
#lang racket
(define text (file->string "/proc/meminfo"))
(for ([line (string-split text "\n")])
(when (regexp-match? #rx"Mem" line)
(displayln line)))
这会打印完整的匹配行,例如:
MemTotal: 11719360 kB
MemFree: 6641244 kB
MemAvailable: 9034448 kB
(具体数字取决于你的机器。这些函数匹配并打印整行,而不仅仅是标签。)
命令行程序:
#lang racket
(command-line #:args (name)
(printf "Hello ~a\n" name))
像这样运行它:
racket hello.rkt Racket
示例输出:
Hello Racket
一个函数式管道。它读取一个包含数字的文件,删除空行,将文本转换为数字,然后将它们全部相加:
(define total
(foldl +
0
(map string->number
(filter (lambda (s) (not (string=? s "")))
(string-split (file->string "numbers.txt") "\n")))))
一旦 map、filter 和 foldl 变得自然,像这样的管道读起来几乎就像是对任务的简单描述,而不是一组实现步骤。
一个微型 Web 服务器。Racket 附带了一个完整的 Web 应用库:
#lang racket
(require web-server/servlet
web-server/servlet-env)
(define (my-app req)
(response/xexpr '(html (body "Hello from Racket"))))
(serve/servlet my-app #:servlet-regexp #rx"")
一个服务处理程序必须返回一个正确的响应值,而不是一个纯字符串,所以这个版本使用 response/xexpr 包装文本。还需要 #:servlet-regexp #rx"" 选项,以便服务器在下面的普通地址上响应,而不是仅在其默认路径上响应。
将上述代码复制到一个文件中,例如 http.rkt,然后运行它:
$ racket http.rkt
Your Web application is running at http://localhost:8000/servlets/standalone.rkt.
Stop this program at any time to terminate the Web Server.
现在,你会看到 Web 服务器在 http://localhost:8000/ 上运行。一个只有几行代码的可用 Web 服务器。
按 CTRL+C 停止 Web 服务器。
17. 条件(多分支条件判断)
if 只处理两个分支。cond 处理多个分支,真正的 Racket 代码使用它而不是嵌套的 if。
(define (sign n)
(cond
[(> n 0) "positive"]
[(< n 0) "negative"]
[else "zero"]))
(sign -5)
示例输出:
"negative"
每一行按顺序检查。第一个为真的测试获胜,else 捕获所有 else 的情况。
18. 和
let 创建局部变量。所有值都是先计算出来的,所以一个变量不能使用另一个变量。
(let ([x 1] [y 2])
(+ x y))
示例输出:
3
let* 让每个变量使用在其之前定义的变量。
(let* ([x 1] [y (+ x 1)])
(+ x y))
示例输出:
3
在普通的 let 中编写 (y (+ x 1)) 会失败,因为 x 尚未就绪。
19. 向量
列表最适合从前面逐个添加元素。当你需要快速、按索引访问时,向量是最佳选择,并且它们可以原地更改。
(define v (vector 1 2 3))
(vector-ref v 0)
示例输出:
1
(vector-set! v 0 99)
(vector-ref v 0)
示例输出:
99
20. 基本错误处理
error 引发一个异常。with-handlers 捕获它,这样你的程序就可以做出响应,而不是崩溃。
(with-handlers ([exn:fail? (lambda (e) (displayln (exn-message e)))])
(error "Something went wrong"))
示例输出:
Something went wrong
你现在已经看到了塑造 Racket 代码编写方式的核心概念:表达式和一等函数、map/filter/fold、递归、模式匹配、宏、契约,以及足以编写真正工具的 Linux 脚本编写知识。
这些内容不会让你成为专家。但它为你提供了对该语言的工作 map,这样当你接下来阅读 Racket 指南 和 Racket 参考手册 时,它们会更有意义。从这里开始,最好的学习方法是编写小程序,并在过程中查阅资料。
祝你好运!



