Common Lisp:如何让外部宏的符号作用域适配当前包?
如何让宏的私有绑定符号在跨包调用时无需前缀?
我想在my-utils包中定义一个工具宏n-collect,宏的局部作用域里绑定了符号n作为迭代索引。希望从其他包调用这个宏时,不用给n加包名前缀,同时还要保持n是宏的私有符号(不导出)。请问这可行吗?
示例代码:
(defpackage #:my-utils (:use #:cl) (:export #:N-COLLECT)) (in-package :my-utils) (defmacro n-collect (n form &key (initial-element '())) "返回一个将form求值n次得到的元素前置到initial-element的列表,其中符号n在form的词法作用域中绑定为迭代索引。" `(labels ((fn (idx seq) (if (< idx ,n) (cons (let ((n idx)) (declare (ignorable n)) ,form) (fn (+ idx 1) seq)) seq))) (fn 0 ,initial-element))) (n-collect 10 n) ; => (0 1 2 3 4 5 6 7 8 9) (in-package :cl-user) (my-utils:n-collect 10 my-utils::n) ; => (0 1 2 3 4 5 6 7 8 9) (my-utils:n-collect 10 n) ; -> 报错:变量N未绑定
我知道可以导出n并(use-package :my-utils)解决,但更希望n是宏的私有符号。
可行,你可以利用宏展开阶段的符号替换来实现,不用导出n。核心思路是:在宏展开时,把用户传入的form里所有名为N的符号,自动替换成my-utils包的私有n,这样用户写n时,宏展开后会指向正确的局部绑定。
修改后的宏代码如下(递归遍历替换符号的方式,能处理嵌套结构的form,适用性更广):
(defpackage #:my-utils (:use #:cl) (:export #:N-COLLECT)) (in-package :my-utils) (defun tree-replace (fn tree) "递归遍历树形结构,对每个符号应用fn函数" (cond ((consp tree) (cons (tree-replace fn (car tree)) (tree-replace fn (cdr tree)))) ((symbolp tree) (funcall fn tree)) (t tree))) (defmacro n-collect (n form &key (initial-element '())) "返回一个将form求值n次得到的元素前置到initial-element的列表,其中符号n在form的词法作用域中绑定为迭代索引。" (let ((local-n (intern "N" *package*))) ; 获取本包的私有符号N (let ((processed-form (tree-replace (lambda (sym) (if (string= (symbol-name sym) "N") local-n sym)) form))) `(labels ((fn (idx seq) (if (< idx ,n) (cons (let ((n idx)) (declare (ignorable n)) ,processed-form) (fn (+ idx 1) seq)) seq))) (fn 0 ,initial-element)))))
现在在cl-user包调用:
(my-utils:n-collect 10 n) ; => (0 1 2 3 4 5 6 7 8 9)
完全正常,而且my-utils::n依然是私有符号,不需要导出。
原理很简单:宏在my-utils包中展开,我们遍历用户传入的form,把所有名字是N的符号(不管用户写的是哪个包的n)都替换成本包的私有n。这样宏展开后的代码里,form中的n就指向了let绑定的局部变量,自然不会报错。
如果需求简单,也可以用sublis做快速替换,但递归遍历的方式能处理更复杂的嵌套表达式。
内容的提问来源于stack exchange,提问作者Orm Finnendahl
相关产品推荐
相关产品推荐

