第7章:宏系统
第 7 章:宏系统
7.1 宏基础
7.1.1 什么是宏
宏(Macro)是编译期的代码转换机制——它接收代码作为输入,输出转换后的代码。宏在求值之前进行展开,允许程序员扩展语言本身的语法。
┌─ 宏的工作流程 ─────────────────────────────────┐
│ │
│ 源代码 宏展开 求值 │
│ ┌────────┐ ┌──────────┐ ┌────────┐ │
│ │ (my-if │ → │ (cond │ → │ 结果 │ │
│ │ #t │ │ (#t │ │ │ │
│ │ 1 2) │ │ 1) │ │ │ │
│ └────────┘ │ (else │ └────────┘ │
│ │ 2)) │ │
│ └──────────┘ │
│ 宏在编译期展开, │
│ 不影响运行时性能 │
│ │
└─────────────────────────────────────────────────┘
7.1.2 宏与函数的区别
| 特性 | 函数 | 宏 |
|---|---|---|
| 求值时机 | 运行时 | 编译期(展开期) |
| 参数求值 | 先求值所有参数 | 参数不预先求值 |
| 返回值 | 计算结果 | 代码(S-表达式) |
| 调试 | 简单 | 较复杂 |
| 性能 | 有调用开销 | 展开后无额外开销 |
| 类型 | 值 | 语法转换器 |
;; 函数:参数在调用前已求值
(define (my-if test then else)
(cond (test then)
(else else)))
;; 问题:then 和 else 都会被求值!
(my-if #t (display "yes") (display "no"))
;; 输出: yesno(两个分支都执行了)
;; 宏:可以选择性地展开
;; 宏定义见下节
7.2 syntax-rules 宏
syntax-rules 是 Scheme 标准的卫生宏(hygienic macro)机制。
7.2.1 基本语法
;; syntax-rules 的基本结构
;; (syntax-rules (字面量...)
;; (模式 模板)
;; (模式 模板)
;; ...)
;; 模式匹配:_ 匹配任何内容
(define-syntax my-when
(syntax-rules ()
((_ test body ...)
(if test (begin body ...)))))
(my-when (> 3 2)
(display "3 大于 2")
(newline))
;; 展开为: (if (> 3 2) (begin (display "3 大于 2") (newline)))
7.2.2 模式变量与省略号
;; ... 表示"零个或多个"
(define-syntax my-list
(syntax-rules ()
((_ item ...)
(list item ...))))
(my-list 1 2 3 4)
;; 展开为: (list 1 2 3 4)
;; 多组省略号
(define-syntax my-pairs
(syntax-rules ()
((_ (key val) ...)
(list (cons key val) ...))))
(my-pairs (a 1) (b 2) (c 3))
;; 展开为: (list (cons 'a 1) (cons 'b 2) (cons 'c 3))
;; 注意:symbol 在这里不会自动引号化,需要额外处理
;; 字面量匹配
(define-syntax my-cond
(syntax-rules (else)
((_ (else body ...))
(begin body ...))
((_ (test body ...) rest ...)
(if test
(begin body ...)
(my-cond rest ...)))))
(my-cond
((> 3 5) "大于")
((= 3 5) "等于")
(else "小于"))
7.2.3 卫生宏(Hygienic Macros)
卫生宏是 Scheme 宏系统的核心特性——宏内部引入的变量不会与外部代码冲突。
;; 卫生宏的关键特性
(define-syntax swap!
(syntax-rules ()
((_ a b)
(let ((temp a)) ; temp 不会与外部的 temp 冲突
(set! a b)
(set! b temp)))))
(define temp 100)
(define x 1)
(define y 2)
(swap! x y)
;; x => 2, y => 1, temp => 100(temp 不受影响)
;; 非卫生宏的问题(Common Lisp 风格)
;; 如果 temp 不是卫生的,上面的 temp 100 会被覆盖
;; 对比:使用 gensym 确保唯一性(了解原理)
;; (define-syntax swap-unsafe
;; (syntax-rules ()
;; ((_ a b)
;; (let ((t a)) ; t 可能与外部冲突!
;; (set! a b)
;; (set! b t)))))
注意:
syntax-rules自动保证卫生性,无需手动管理变量名冲突。这是 Scheme 宏优于 Common Lispdefmacro的关键所在。
7.3 define-syntax 进阶
7.3.1 多子句匹配
;; 带多子句的宏
(define-syntax my-let
(syntax-rules ()
;; 简单 let
((_ ((var val) ...) body ...)
((lambda (var ...) body ...) val ...))
;; 命名 let
((_ name ((var val) ...) body ...)
((letrec ((name (lambda (var ...) body ...)))
name)
val ...))))
;; 使用简单 let
(my-let ((x 1) (y 2))
(+ x y))
;; => 3
;; 使用命名 let
(my-let loop ((i 0) (acc 0))
(if (> i 10)
acc
(loop (+ i 1) (+ acc i))))
;; => 55
7.3.2 递归宏
;; 递归展开的宏
(define-syntax or-mine
(syntax-rules ()
((_) #f)
((_ e) e)
((_ e1 e2 ...)
(let ((t e1))
(if t t (or-mine e2 ...))))))
(or-mine #f #f 3 #f)
;; => 3
;; 条件编译宏
(define-syntax define-with-log
(syntax-rules ()
((_ (name args ...) body ...)
(define (name args ...)
(format #t "调用 ~a~%" 'name)
body ...))))
(define-with-log (add a b)
(+ a b))
(add 3 4)
;; 输出: 调用 add
;; => 7
7.3.3 字面量集
;; 使用字面量集(literal sets)精确控制匹配
(define-syntax my-case
(syntax-rules (else =>)
((_ expr)
#f)
((_ expr (else body ...))
(begin body ...))
((_ expr (vals => func) rest ...)
(if (memv expr 'vals)
(func expr)
(my-case expr rest ...)))
((_ expr (vals body ...) rest ...)
(if (memv expr 'vals)
(begin body ...)
(my-case expr rest ...)))))
(my-case 2
((1 3 5) "奇数")
((2 4 6) "偶数" "是偶数")
(else "其他"))
;; => "偶数" "是偶数"(返回最后一个表达式的结果)
7.4 自定义宏实战
7.4.1 常用宏模式
;; 1. 表达式计时宏
(define-syntax time-it
(syntax-rules ()
((_ label body ...)
(let ((start (get-internal-real-time)))
(let ((result (begin body ...)))
(let ((end (get-internal-real-time)))
(format #t "[~a] 耗时: ~a ms~%"
label
(/ (* (- end start) 1000)
internal-time-units-per-second))
result))))))
(time-it "计算"
(apply + (iota 1000000)))
;; 输出: [计算] 耗时: XX ms
;; => 499999500000
;; 2. 断言宏
(define-syntax assert
(syntax-rules ()
((_ expr)
(unless expr
(error "断言失败:" 'expr)))
((_ expr msg)
(unless expr
(error msg 'expr)))))
(define x 5)
(assert (> x 0)) ; 通过
;; (assert (< x 0)) ; => ERROR: 断言失败: (< x 0)
;; 3. 解构绑定宏
(define-syntax let-alist
(syntax-rules ()
((_ alist ((key var) ...) body ...)
(let ((var (assq-ref alist 'key)) ...)
body ...))))
(define config '((host . "localhost") (port . 8080) (debug . #t)))
(let-alist config ((host h) (port p) (debug d))
(format #t "~a:~a debug=~a~%" h p d))
;; 输出: localhost:8080 debug=#t
;; 4. 组合子宏
(define-syntax compose-macros
(syntax-rules ()
((_ f g)
(syntax-rules ()
((_ args ...)
(f (g args ...)))))))
;; 5. 条件定义宏
(define-syntax define-if-available
(syntax-rules ()
((_ name module-ref fallback)
(define name
(catch #t
(lambda ()
(resolve-interface module-ref))
(lambda _ fallback))))))
7.4.2 编译期计算
;; 在宏展开期进行计算
(define-syntax compile-time-constant
(syntax-rules ()
((_ expr)
(let-syntax ((val (syntax-rules ()
((_) expr))))
(val)))))
;; 实际应用:编译期字符串拼接
(define-syntax string-join-compile
(syntax-rules ()
((_ sep s ...)
(string-append s ...))))
;; 注:Guile 的 syntax-rules 能力有限,
;; 复杂编译期计算需要 syntax-case 或 define-macro
7.5 define-macro(传统宏)
Guile 也支持传统(非卫生)的 define-macro,类似于 Common Lisp 的 defmacro。
;; define-macro(非卫生,需谨慎使用)
(define-macro (my-unless test . body)
`(if (not ,test)
(begin ,@body)))
(my-unless (< 3 2)
(display "3 不小于 2")
(newline))
;; 展开为: (if (not (< 3 2)) (begin (display "3 不小于 2") (newline)))
;; 对比 syntax-rules 和 define-macro
;; syntax-rules 版本(卫生)
(define-syntax s-unless
(syntax-rules ()
((_ test body ...)
(if (not test) (begin body ...)))))
;; define-macro 版本(非卫生,但更灵活)
(define-macro (m-unless test . body)
`(if (not ,test)
(begin ,@body)))
;; 非卫生宏的风险
(define-macro (dangerous-swap! a b)
`(let ((temp ,a))
(set! ,a ,b)
(set! ,b temp)))
;; 如果外部有 temp 变量会出问题
(define temp 100)
(define p 1)
(define q 2)
(dangerous-swap! p q)
;; temp 可能被覆盖!
注意:优先使用
syntax-rules,只在syntax-rules无法实现时才使用define-macro。
7.6 syntax-case(高级宏)
syntax-case 是更强大的宏系统,支持运行时检查和代码生成。
(use-modules (ice-9 syncase))
;; syntax-case 允许在模式匹配中使用任意谓词
(define-syntax my-letrec
(lambda (stx)
(syntax-case stx ()
((_ ((var init) ...) body ...)
(with-syntax ((tmp (generate-temporaries #'(var ...))))
#'((lambda (var ...)
(let ((tmp init) ...)
(set! var tmp) ...
body ...))
(void) ...))))))
;; 带谓词的模式匹配
(define-syntax my-define
(lambda (stx)
(syntax-case stx ()
((_ name value)
(identifier? #'name)
#'(define name value))
((_ (name args ...) body ...)
(identifier? #'name)
#'(define (name args ...) body ...)))))
;; syntax-case 的优势:可以进行任意检查
(define-syntax check-type
(lambda (stx)
(syntax-case stx (integer string)
((_ integer expr)
#'(let ((val expr))
(unless (integer? val)
(error "期望整数" val))
val))
((_ string expr)
#'(let ((val expr))
(unless (string? val)
(error "期望字符串" val))
val)))))
(check-type integer 42) ; => 42
;; (check-type integer "hi") ; => ERROR: 期望整数
7.7 宏调试
7.7.1 展开查看
;; 查看宏展开结果
(use-modules (ice-9 pretty-print))
;; 使用 macroexpand(Guile 中通过 Ice-9 提供)
(use-modules (system syntax internal))
;; 或者简单地在 REPL 中使用 ,pp
;; scheme@(guile-user)> ,pp (syntax->datum (expand '(my-when #t (display "hi"))))
;; 手动展开
(define-syntax test-macro
(syntax-rules ()
((_ x) (+ x 1))))
;; 调试技巧:用 quote 返回宏的输出
(define-syntax debug-macro
(syntax-rules ()
((_ x)
(quote (+ x 1))))) ; 返回代码而非执行结果
(debug-macro 5) ; => (+ x 1)
7.7.2 常见宏错误
| 错误 | 原因 | 解决方案 |
|---|---|---|
Bad syntax | 模式不匹配 | 检查模式中的变量和字面量 |
Unbound variable | 卫生性导致变量名不同 | 使用 syntax-case 或重新设计 |
| 无限展开 | 递归宏没有终止条件 | 确保有基本情况 |
| 变量捕获 | 使用 define-macro 时 | 改用 syntax-rules |
7.8 业务场景
7.8.1 DSL(领域特定语言)
;; 用宏创建 HTML DSL
(define-syntax html
(syntax-rules (div p span h1 h2 ul li a)
((_ (div attrs ... body ...))
(string-append "<div" (format-attrs attrs ...) ">"
(html body ...) ... "</div>"))
((_ (p body ...))
(string-append "<p>" (html body ...) ... "</p>"))
((_ (h1 body ...))
(string-append "<h1>" (html body ...) ... "</h1>"))
((_ text)
(if (string? text) text (format #f "~a" text)))))
;; 实用的配置 DSL
(define-syntax define-config
(syntax-rules ()
((_ name (key default) ...)
(define name
(let ((ht (make-hash-table)))
(hash-set! ht 'key default) ...
(lambda (msg . args)
(case msg
((get) (hash-ref ht (car args)))
((set!) (hash-set! ht (car args) (cadr args)))
((all) ht))))))))
(define-config my-app-config
(host "localhost")
(port 8080)
(debug #f))
(my-app-config 'get 'host) ; => "localhost"
(my-app-config 'set! 'port 3000)
(my-app-config 'get 'port) ; => 3000
7.8.2 测试框架宏
;; 简易测试框架
(define test-count 0)
(define pass-count 0)
(define-syntax test-case
(syntax-rules ()
((_ name expr expected)
(begin
(set! test-count (+ test-count 1))
(let ((result expr))
(if (equal? result expected)
(begin
(set! pass-count (+ pass-count 1))
(format #t " ✓ ~a~%" name))
(format #t " ✗ ~a: 期望 ~a, 得到 ~a~%"
name expected result)))))))
(define-syntax test-suite
(syntax-rules ()
((_ name test ...)
(begin
(format #t "=== ~a ===~%" name)
test ...
(format #t "结果: ~a/~a 通过~%"
pass-count test-count)
(set! test-count 0)
(set! pass-count 0)))))
(test-suite "算术测试"
(test-case "加法" (+ 1 2) 3)
(test-case "乘法" (* 3 4) 12)
(test-case "减法" (- 10 3) 7))
;; === 算术测试 ===
;; ✓ 加法
;; ✓ 乘法
;; ✓ 减法
;; 结果: 3/3 通过
7.9 本章小结
| 主题 | 要点 |
|---|---|
| 宏是什么 | 编译期代码转换,操作代码本身 |
| syntax-rules | 卫生宏,推荐使用 |
| define-macro | 非卫生宏,灵活性高但需谨慎 |
| syntax-case | 高级宏,支持谓词和代码生成 |
| 卫生性 | Scheme 宏系统的核心优势 |
| 宏调试 | 查看展开结果,理解错误信息 |
扩展阅读
上一章:第 6 章:控制流 下一章:第 8 章:数据结构