Skip to content

LISP - 宏的用途

宏(macro)是 Lisp 元编程能力(metaprogramming)的基石。函数操作数据(值),而宏操作代码本身,在代码编译或求值之前对其进行转换。这允许你扩展 Lisp 语言,创建新的语法和控制结构。让我们通过实际示例来探讨宏最常见和强大的用途。

宏可以定义控制执行流程的新方式。一个经典的例子是“回指 if”(anaphoric if,aif),它会评估一个测试条件,如果结果不是 NIL,则将其结果绑定到特殊变量 it。

(defmacro aif (test-form then-form &optional else-form)
"回指 if:在 then-form 中将 test-form 的结果绑定到 'it'。"
`(let ((it ,test-form))
(if it
,then-form
,else-form)))
;; 没有 aif,你需要一个临时变量或第二次调用
(let ((pos (position #\a "banana")))
(if pos
(format t "Found 'a' at index: ~a~%" pos)))
;; 使用 aif,代码更简洁
(aif (position #\a "banana")
(format t "Found 'a' at index: ~a~%" it))

宏擅长自动化重复的代码模式,减少冗余和出错的可能性。例如,一个宏可以一次定义多个相关函数。

(defmacro define-binary-op (op-name op-symbol)
"定义一个函数,该函数将二元操作应用于数字列表。"
`(defun ,op-name (numbers)
(reduce #',op-symbol numbers)))
;; 现在我们可以快速定义求和与求积函数
(define-binary-op sum-list +)
(define-binary-op product-list *)
(sum-list '(1 2 3 4)) ; -> 10
(product-list '(1 2 3 4)) ; -> 24

宏的一个关键用途是创建能够保证清理操作(例如关闭文件)的形式。内置的 with-open-file 就是一个宏。如果函数体内部发生错误,函数无法保证文件被关闭。宏会将函数体封装在确保清理的代码中。

;; with-open-file 的概念结构
(defmacro with-open-file ((stream-var filespec-options) &body body)
`(let ((,stream-var (open ,@filespec-options)))
(unwind-protect
(progn ,@body)
(close ,stream-var))))

unwind-protect 确保无论代码体如何退出——无论是正常退出、通过错误还是非局部退出——都会调用 (close stream)。这使得资源处理对用户来说既安全又简单。

(with-open-file (my-stream "data.txt" :direction :output)
(write-line "Hello, World!" my-stream)
;; my-stream 在此自动关闭
)

宏可以用于在 Lisp 内部嵌入小型、专用语言,使特定领域的代码更具表现力。一个常见的例子是 HTML 生成的 DSL(领域特定语言)。

(defmacro with-html-tags ((stream) &body tags)
`(progn
,@(mapcar (lambda (tag)
`(format ,stream "<~a>~a</~a>"
',(first tag)
(second tag)
',(first tag)))
tags)))
(with-output-to-string (s)
(with-html-tags (s)
(h1 "My Web Page")
(p "Welcome to my simple page.")))
<h1>My Web Page</h1><p>Welcome to my simple page.</p>

宏功能非常强大,但应谨慎使用。基本原则是:如果能写成函数,就写成函数。 仅当你需要执行函数无法完成的操作时才使用宏:

  • 控制求值:你需要执行某些代码而不对其所有参数进行求值,或者以特殊方式对其进行求值(例如 if、and、or)。
  • 创建新语法:你想引入一种不遵循标准函数调用语义的新语言结构。
  • 执行编译时计算:你需要在编译阶段生成或优化代码。