Racket 编程入门

第 7 部分 · 走向生产:工程化与实战

编码规范——别把 Lisp 写成只写语言

Lisp 的极简语法是双刃剑——没有规范,代码很快变成谁也读不懂的"只写语言"。这篇基于官方风格指南,提炼出可落地的编码规范。

Lisp 的语法简洁是把双刃剑:写起来快,读起来快不快,全看代码规不规矩。没有规矩,简洁会反向发酵——(car (cdr (cdr lst))) 这样的表达式,六个月后连作者本人都认不出它想干什么,别人更不用提。

所以这篇不讲新特性,讲规矩:基于官方风格指南,挑出最值得先立起来的几条。

命名与缩进

命名用 kebab-case:validate-input,不是 validateInput,更不是 validate_input。这是 Lisp 世界的通用习惯,Racket 标准库从头到尾都是这个风格。

缩进跟着结构走:每个层级 2 个空格,禁用 Tab 字符。

(define (factorial n)
  (cond [(= n 0) 1]
        [else (* n (factorial (- n 1)))])) ; 递归情况缩进对齐

括号交给编辑器管:括号怎么排不要手写手调——第 50 章的 paredit 就是干这个的,它保证你的括号永远不会失衡。

模块长什么样

一个规矩的模块,合约放在最显眼的位置:

#lang racket

(require racket/contract
         rackunit)

(provide (contract-out
          [string->safe-port-number (-> string? (integer-in 1 65535))]))

;; 将字符串转换为合法的端口号
(define (string->safe-port-number str)
  (define port (string->number str))
  (unless (and (integer? port) (<= 1 port 65535))
    (error 'invalid-port))
  port)

一眼看过去,读的人先看到 provide:这个模块给外界什么、各是什么类型。实现细节全部排在后面。

写法上的两条取舍

cond 优于嵌套 if。多分支时嵌套 if 会越埋越深,cond 把所有分支平铺在同一列:

;; 反模式
(if (positive? x)
    (if (> x 100)
        'large
        'small)
    'negative)

;; 推荐模式
(cond [(positive? x) (if (> x 100) 'large 'small)]
      [else 'negative])

该用 struct 就别用列表凑。列表什么都能装,也就什么都不说明;struct 给每个字段一个名字,读代码的人和工具都不用猜第三个元素是什么:

(struct employee (name id department) #:transparent)
(define john (employee "John Doe" 1001 "R&D"))

注释与测试

注释分两层:文件头说明模块职责,函数前说清「接什么、还什么」:

;; 计算两个坐标点的欧式距离
;; @param p1 点1 (cons x y)
;; @param p2 点2 (cons x y)
;; @return 两点间的直线距离
(define (distance p1 p2) ...)

测试就近放在模块的 module+ test 里,用 rackunit:

(module+ test
  (require rackunit)
  (check-equal? (add1 41) 42)
  (check-exn exn:fail? (λ () (string->safe-port-number "99999"))))

测试跟代码住在同一个文件里,改函数的时候顺手就能跑。

让规范自己长出来

规范立起来之后,靠三样东西维持:把规则写进 .editorconfig 和静态检查;新代码严格执行,旧代码改到哪儿就顺手适配到哪儿;CI 里把风格检查自动化。

规范的价值不在于约束,而在于让开发者专注于真正重要的逻辑设计。好的 Racket 代码应该像数学证明一样优雅自明。

工程化的工具和规范讲到这里,你已经具备把代码组织好、写整洁的能力。下一篇开始进入应用开发的实战——先从最基础的需求讲起:程序写出来的数据,怎么存下来。