评估传递给在 lisp 中生成函数的宏的参数
Evaluate arguments passed to a macro that generates functions in lisp
我正在尝试制作一个宏,根据 py-configparser
创建的配置对象为每个配置定义访问器函数:
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-"
(str:concat "get-" option))))))
`(defun ,fun-name (config)
(py-configparser:get-option config ,section ,option))))
如果 option
作为字符串传入,它工作正常,但当它是像 (car ("db" . "test.db"))
这样的一对时,表单按原样传递并导致错误。我如何评估宏中的 option
参数,不使用 eval
.
完整示例:假设我有一个 test.ini
文件:
[Settings]
db = "test.db"
使用py-configparser
(可以用(ql:quickload "py-configparser")
安装,可以将config文件转成Lisp对象:
(setf *test-config* (py-configparser:make-config))
(py-configparser:read-files *test-config* '("~/test.ini"))
这应该是输出:
#S(PY-CONFIGPARSER:CONFIG
:DEFAULTS #S(PY-CONFIGPARSER::SECTION :NAME "DEFAULT" :OPTIONS NIL)
:SECTIONS (#S(PY-CONFIGPARSER::SECTION
:NAME "Settings"
:OPTIONS (("db" . "\"test.db\""))))
:OPTION-NAME-TRANSFORM-FN #<FUNCTION STRING-DOWNCASE>
:SECTION-NAME-TRANSFORM-FN #<FUNCTION IDENTITY>)
("~/test.ini")
然后,您可以像这样检索 db
选项:
(py-configparser:get-option *test-config* "Settings" "db")
输出:
"\"test.db\""
现在我正在编写一个宏来为每个选项创建一个函数,例如 db
,(get-db *test-config*)
应该给我相同的输出。
我用上面的 make-config-accessor
宏让它工作,但是当我传递像 (car ("db" . "test.db"))
这样的表格时,我必须使用 eval
否则 str:concat
失败。
我制作了一个 gen-accessors
循环遍历配置对象中的每个选项并为其生成访问器:
(defun gen-accessors (config)
(let ((sections (py-configparser:sections config)))
(loop for s in sections
do (loop for i in (py-configparser:items config s)
do (let* ((o (car i)))
(make-config-accessor config s o))))))
这是一种罕见的情况,您必须将 eval
与反引号宏调用结合使用并取消引用参数。
(我无意中发现了这个构造,并自己称它为 eval-over-macro-call
。- 遵循命名传统 let-over-lambda
。- 实际上它应该被命名为 eval-over-backquoted-macro-call-with-unquoting
。它允许你使用动态宏。Vsevolod Dyomkin 也 stumbled over it,独立地。我回答了他,因为我大约在同一时间或之前偶然发现了它。宏 - 正如你所意识到的 - 不允许对评估进行任意控制。)
但首先,我生成了一些辅助函数。
(你可以使用你的 :str
包函数,但我在安装它时遇到了问题。依赖越少越好。就我个人而言,我更喜欢 cl-ppcre
用于替换等。
但是,在您的情况下,可以摆脱任何依赖关系。
intern
污染了你的命名空间。您只希望函数名称空间具有 get-
函数名称条目。但不是变量命名空间。因此,只有 return 个符号而不自动将它们驻留,请使用 read-from-string
.
dotted-list-p
函数需要 :alexandria
包。然而,无论如何,大多数人都需要它,因为它是 common lisp shpere 中最常用的包之一(与 :cl-ppcre
一起)我认为这不算作“额外的依赖”。
对于 dotted-pair-p
函数,我必须进行一些搜索。
dotted-list-to-list
转换器函数,我自己写的
如果您为 options
.
使用简单的字符串列表,您可以删除所有 dotted-list
函数
在那种情况下,在宏中,只需使用 listp
而不是 dotted-list-p
。
并使用 option
而不是 (dotted-list-to-list option)
.
;; one character replacement
(substitute #\+ #\Space "a simple example")
replacer find obj
(defun string-to-upper-symbol (str)
(read-from-string (substitute #\- #\_ (format nil "get-~A" str))))
(ql:quickload :alexandria)
(defun dotted-list-p (x)
(and (not (alexandria:proper-list-p x))
(consp x)))
;; correct - but gives nil if empty list (or (null x) ...) would include empty list
(defun dotted-or-empty-list-p (x)
(or (null x) (dotted-list-p x)))
;; this gives t for empty list and dotted lists
(defun dotted-pair-p (x)
(and (not (listp (cdr x))) (consp x)))
(defun dotted-list-to-list (dotted-list &optional (acc '()))
(cond ((null dotted-list) (nreverse acc))
((dotted-pair-p dotted-list) (dotted-list-to-list '() (cons (cdr dotted-list)
(cons (car dotted-list)
acc))))
(t (dotted-list-to-list (cdr dotted-list) (cons (car dotted-list) acc)))))
您的宏包含在参数列表 config
中,但从未使用过。
如果您只是忘记取消引用宏中的 config
,正确的解决方案是:
(defmacro %make-config-accessor (config section option)
; create an upper case function name then intern
(let* ((fun-name (string-to-upper-symbol option)))
`(defun ,fun-name (,config)
(py-configparser:get-option ,config ,section ,option)))))
(defun make-config-accessor (config section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(%make-config-accessor ,config ,section ,x)))
(%make-config-accessor config section option)))
;; call with
;; (make-config-accessor '<your-config> '<your-section> '("option1" "option2" . "option3"))
;; test for existence
;; #'get-option1
;; #'get-option2
;; #'get-option3
在另一种情况下,您不需要配置,正确的解决方案是:
(defmacro %make-config-accessor (section option)
; create an upper case function name then intern
(let* ((fun-name (string-to-upper-symbol option)))
`(defun ,fun-name (config)
(py-configparser:get-option config ,section ,option)))))
(defun make-config-accessor (section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(%make-config-accessor ,section ,x)))
(%make-config-accessor section option)))
;; call with
;; (make-config-accessor '<your-section> '("option1" "option2" . "option3"))
;; test for existence
;; #'get-option1
;; #'get-option2
;; #'get-option3
注意,因为你需要一个函数,所以你必须在调用中引用参数 config
和 section
(它们在函数中等待评估 option
得到评估。
感谢 quote
和 backquote
以及 unquote
和 eval
,您可以完全控制 lisp 中的评估级别。
有时,如果想要在参数列表中使用更多 quote
控制多轮评估。
您也可以将辅助宏和函数融合到一个宏中。
但是,那么,每次调用宏时,都必须使用这个
eval-over-backquoted-macro-call
取消引用所需的参数。
(defmacro make-config-accessor (section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(make-config-accessor ,section ,x)))
`(defun ,(string-to-upper-symbol c) (config)
(py-configparser:get-option config ,section ,option))))
;; call it with
;; (eval `(make-config-accessor <your-section> ,<your-option>))
;; e.g.
;; (eval `(make-config-accessor <your-section> ,'("opt1" "opt2" . "opt3")))
;; test existence with
;; #'get-opt1
;; #'get-opt2
;; #'get-opt3
顺便说一句。我不再相信这种“eval
被禁止”的说法。
在这种情况下 - 主要是宏中的评估控制,必须 eval
作为唯一的选择,必须为此问题编写一个额外的迷你解释器......这会更加乏味(而且很可能也会出现更多错误俯卧撑)。
您没有提供可行的代码。所以我不得不用一些东西来弄清楚这一切 functions/macros,我写道。
(defmacro q (b c)
`(defun ,(string-to-upper-symbol c) (a) (list a ,b ,c)))
(defun q-fun (b c)
(if (dotted-list-p c)
(loop for x in (dotted-list-to-list c)
do (eval `(q ,b ,x)))
(q b c)))
;; (q "b" "c")
;; (q "b" '("d" . "e"))
;; (macroexpand-1 '(q "b" '("d" . "e")))
(defmacro p (b c)
(if (dotted-list-p c)
(loop for x in (dotted-list-to-list c)
do (eval `(p ,b ,x)))
`(defun ,(string-to-upper-symbol c) (a) (list a ,b ,c))))
您需要两个级别的评估。
尝试:
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
`(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-" (str:concat "get-" ,option))))))
(eval `(defun ,fun-name (config)
(py-configparser:get-option config ,,section ,,option)))))
现在 option
以 let*
形式计算。
然后需要使用 eval
.
评估返回的 defun
形式(始终在全局范围内,或空词法环境或顶层)
这就是我正确 运行 您的代码所需的所有更改。
仅供参考,我在此处添加 运行 的整个代码(注意:gen-accessors
有变化,我认为您打算使用 config
而不是 *config*
) .
(ql:quickload "str")
(ql:quickload "py-configparser")
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
`(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-"
(str:concat "get-" ,option))))))
(eval `(defun ,fun-name (config)
(py-configparser:get-option config ,,section ,,option)))))
(defun gen-accessors (config)
(let ((sections (py-configparser:sections config)))
(loop for s in sections
do (loop for i in (py-configparser:items config s)
do (let* ((o (car i)))
(make-config-accessor config s o))))))
(setf *test-config* (py-configparser:make-config))
(py-configparser:read-files *test-config* '("~/Desktop/test.ini"))
(gen-accessors *test-config*)
(get-db *test-config*)
编写宏的第一条规则是:如果您发现自己在使用 eval
,那么几乎可以肯定您犯了一个错误。在这种情况下,你犯的错误是你根本不需要宏:你想要一个函数。
特别是您可能需要此功能或类似功能:
(defun make-config-accessor (section option)
;; Make an accessor for OPTION in SECTION with a suitable name
(let ((fun-name (intern (nsubstitute #\- #\_
(format nil "GET-~A"
(string-upcase option))))))
(setf (symbol-function fun-name)
(lambda (config)
(py-configparser:get-option config section option)))
fun-name)))
然后给定一个合适的配置reader
(defun read-config (&rest files)
(py-configparser:read-files (py-configparser:make-config)
files))
连同你的 gen-accessors
:
的一个相当简化的(更少的一次性绑定)版本
(defun gen-accessors (config)
(loop for s in (py-configparser:sections config)
appending (loop for i in (py-configparser:items config s)
collect (make-config-accessor s (car i)))))
然后,例如如果 /tmp/x.ini
包含
[Settings]
db = "test.db"
scrunge = 12
然后
> (gen-accessors (read-config "/tmp/x.ini"))
(get-scrunge get-db)
> (get-scrunge (read-config "/tmp/x.ini"))
"12"
你可以用这样的方式定义 make-config-accessor
甚至更好:
(defun curryr (f &rest trailing-args)
(lambda (&rest args)
(declare (dynamic-extent args))
(apply f (append args trailing-args))))
(defun make-config-accessor (section option)
;; Make an accessor for OPTION in SECTION with a suitable name
(let ((fun-name (intern (nsubstitute #\- #\_
(format nil "GET-~A"
(string-upcase option))))))
(setf (symbol-function fun-name)
(curryr #'py-configparser:get-option section option))
fun-name))
当然,并不是每个人都会觉得这更好。
我正在尝试制作一个宏,根据 py-configparser
创建的配置对象为每个配置定义访问器函数:
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-"
(str:concat "get-" option))))))
`(defun ,fun-name (config)
(py-configparser:get-option config ,section ,option))))
如果 option
作为字符串传入,它工作正常,但当它是像 (car ("db" . "test.db"))
这样的一对时,表单按原样传递并导致错误。我如何评估宏中的 option
参数,不使用 eval
.
完整示例:假设我有一个 test.ini
文件:
[Settings]
db = "test.db"
使用py-configparser
(可以用(ql:quickload "py-configparser")
安装,可以将config文件转成Lisp对象:
(setf *test-config* (py-configparser:make-config))
(py-configparser:read-files *test-config* '("~/test.ini"))
这应该是输出:
#S(PY-CONFIGPARSER:CONFIG
:DEFAULTS #S(PY-CONFIGPARSER::SECTION :NAME "DEFAULT" :OPTIONS NIL)
:SECTIONS (#S(PY-CONFIGPARSER::SECTION
:NAME "Settings"
:OPTIONS (("db" . "\"test.db\""))))
:OPTION-NAME-TRANSFORM-FN #<FUNCTION STRING-DOWNCASE>
:SECTION-NAME-TRANSFORM-FN #<FUNCTION IDENTITY>)
("~/test.ini")
然后,您可以像这样检索 db
选项:
(py-configparser:get-option *test-config* "Settings" "db")
输出:
"\"test.db\""
现在我正在编写一个宏来为每个选项创建一个函数,例如 db
,(get-db *test-config*)
应该给我相同的输出。
我用上面的 make-config-accessor
宏让它工作,但是当我传递像 (car ("db" . "test.db"))
这样的表格时,我必须使用 eval
否则 str:concat
失败。
我制作了一个 gen-accessors
循环遍历配置对象中的每个选项并为其生成访问器:
(defun gen-accessors (config)
(let ((sections (py-configparser:sections config)))
(loop for s in sections
do (loop for i in (py-configparser:items config s)
do (let* ((o (car i)))
(make-config-accessor config s o))))))
这是一种罕见的情况,您必须将 eval
与反引号宏调用结合使用并取消引用参数。
(我无意中发现了这个构造,并自己称它为 eval-over-macro-call
。- 遵循命名传统 let-over-lambda
。- 实际上它应该被命名为 eval-over-backquoted-macro-call-with-unquoting
。它允许你使用动态宏。Vsevolod Dyomkin 也 stumbled over it,独立地。我回答了他,因为我大约在同一时间或之前偶然发现了它。宏 - 正如你所意识到的 - 不允许对评估进行任意控制。)
但首先,我生成了一些辅助函数。
(你可以使用你的 :str
包函数,但我在安装它时遇到了问题。依赖越少越好。就我个人而言,我更喜欢 cl-ppcre
用于替换等。
但是,在您的情况下,可以摆脱任何依赖关系。
intern
污染了你的命名空间。您只希望函数名称空间具有 get-
函数名称条目。但不是变量命名空间。因此,只有 return 个符号而不自动将它们驻留,请使用 read-from-string
.
dotted-list-p
函数需要 :alexandria
包。然而,无论如何,大多数人都需要它,因为它是 common lisp shpere 中最常用的包之一(与 :cl-ppcre
一起)我认为这不算作“额外的依赖”。
对于 dotted-pair-p
函数,我必须进行一些搜索。
dotted-list-to-list
转换器函数,我自己写的
如果您为 options
.
dotted-list
函数
在那种情况下,在宏中,只需使用 listp
而不是 dotted-list-p
。
并使用 option
而不是 (dotted-list-to-list option)
.
;; one character replacement
(substitute #\+ #\Space "a simple example")
replacer find obj
(defun string-to-upper-symbol (str)
(read-from-string (substitute #\- #\_ (format nil "get-~A" str))))
(ql:quickload :alexandria)
(defun dotted-list-p (x)
(and (not (alexandria:proper-list-p x))
(consp x)))
;; correct - but gives nil if empty list (or (null x) ...) would include empty list
(defun dotted-or-empty-list-p (x)
(or (null x) (dotted-list-p x)))
;; this gives t for empty list and dotted lists
(defun dotted-pair-p (x)
(and (not (listp (cdr x))) (consp x)))
(defun dotted-list-to-list (dotted-list &optional (acc '()))
(cond ((null dotted-list) (nreverse acc))
((dotted-pair-p dotted-list) (dotted-list-to-list '() (cons (cdr dotted-list)
(cons (car dotted-list)
acc))))
(t (dotted-list-to-list (cdr dotted-list) (cons (car dotted-list) acc)))))
您的宏包含在参数列表 config
中,但从未使用过。
如果您只是忘记取消引用宏中的 config
,正确的解决方案是:
(defmacro %make-config-accessor (config section option)
; create an upper case function name then intern
(let* ((fun-name (string-to-upper-symbol option)))
`(defun ,fun-name (,config)
(py-configparser:get-option ,config ,section ,option)))))
(defun make-config-accessor (config section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(%make-config-accessor ,config ,section ,x)))
(%make-config-accessor config section option)))
;; call with
;; (make-config-accessor '<your-config> '<your-section> '("option1" "option2" . "option3"))
;; test for existence
;; #'get-option1
;; #'get-option2
;; #'get-option3
在另一种情况下,您不需要配置,正确的解决方案是:
(defmacro %make-config-accessor (section option)
; create an upper case function name then intern
(let* ((fun-name (string-to-upper-symbol option)))
`(defun ,fun-name (config)
(py-configparser:get-option config ,section ,option)))))
(defun make-config-accessor (section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(%make-config-accessor ,section ,x)))
(%make-config-accessor section option)))
;; call with
;; (make-config-accessor '<your-section> '("option1" "option2" . "option3"))
;; test for existence
;; #'get-option1
;; #'get-option2
;; #'get-option3
注意,因为你需要一个函数,所以你必须在调用中引用参数 config
和 section
(它们在函数中等待评估 option
得到评估。
感谢 quote
和 backquote
以及 unquote
和 eval
,您可以完全控制 lisp 中的评估级别。
有时,如果想要在参数列表中使用更多 quote
控制多轮评估。
您也可以将辅助宏和函数融合到一个宏中。
但是,那么,每次调用宏时,都必须使用这个
eval-over-backquoted-macro-call
取消引用所需的参数。
(defmacro make-config-accessor (section option)
(if (dotted-list-p option)
(loop for x in (dotted-list-to-list option)
do (eval `(make-config-accessor ,section ,x)))
`(defun ,(string-to-upper-symbol c) (config)
(py-configparser:get-option config ,section ,option))))
;; call it with
;; (eval `(make-config-accessor <your-section> ,<your-option>))
;; e.g.
;; (eval `(make-config-accessor <your-section> ,'("opt1" "opt2" . "opt3")))
;; test existence with
;; #'get-opt1
;; #'get-opt2
;; #'get-opt3
顺便说一句。我不再相信这种“eval
被禁止”的说法。
在这种情况下 - 主要是宏中的评估控制,必须 eval
作为唯一的选择,必须为此问题编写一个额外的迷你解释器......这会更加乏味(而且很可能也会出现更多错误俯卧撑)。
您没有提供可行的代码。所以我不得不用一些东西来弄清楚这一切 functions/macros,我写道。
(defmacro q (b c)
`(defun ,(string-to-upper-symbol c) (a) (list a ,b ,c)))
(defun q-fun (b c)
(if (dotted-list-p c)
(loop for x in (dotted-list-to-list c)
do (eval `(q ,b ,x)))
(q b c)))
;; (q "b" "c")
;; (q "b" '("d" . "e"))
;; (macroexpand-1 '(q "b" '("d" . "e")))
(defmacro p (b c)
(if (dotted-list-p c)
(loop for x in (dotted-list-to-list c)
do (eval `(p ,b ,x)))
`(defun ,(string-to-upper-symbol c) (a) (list a ,b ,c))))
您需要两个级别的评估。
尝试:
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
`(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-" (str:concat "get-" ,option))))))
(eval `(defun ,fun-name (config)
(py-configparser:get-option config ,,section ,,option)))))
现在 option
以 let*
形式计算。
然后需要使用 eval
.
defun
形式(始终在全局范围内,或空词法环境或顶层)
这就是我正确 运行 您的代码所需的所有更改。
仅供参考,我在此处添加 运行 的整个代码(注意:gen-accessors
有变化,我认为您打算使用 config
而不是 *config*
) .
(ql:quickload "str")
(ql:quickload "py-configparser")
(defmacro make-config-accessor (config section option)
; create an upper case function name then intern
`(let* ((fun-name (intern (string-upcase
(str:replace-all "_" "-"
(str:concat "get-" ,option))))))
(eval `(defun ,fun-name (config)
(py-configparser:get-option config ,,section ,,option)))))
(defun gen-accessors (config)
(let ((sections (py-configparser:sections config)))
(loop for s in sections
do (loop for i in (py-configparser:items config s)
do (let* ((o (car i)))
(make-config-accessor config s o))))))
(setf *test-config* (py-configparser:make-config))
(py-configparser:read-files *test-config* '("~/Desktop/test.ini"))
(gen-accessors *test-config*)
(get-db *test-config*)
编写宏的第一条规则是:如果您发现自己在使用 eval
,那么几乎可以肯定您犯了一个错误。在这种情况下,你犯的错误是你根本不需要宏:你想要一个函数。
特别是您可能需要此功能或类似功能:
(defun make-config-accessor (section option)
;; Make an accessor for OPTION in SECTION with a suitable name
(let ((fun-name (intern (nsubstitute #\- #\_
(format nil "GET-~A"
(string-upcase option))))))
(setf (symbol-function fun-name)
(lambda (config)
(py-configparser:get-option config section option)))
fun-name)))
然后给定一个合适的配置reader
(defun read-config (&rest files)
(py-configparser:read-files (py-configparser:make-config)
files))
连同你的 gen-accessors
:
(defun gen-accessors (config)
(loop for s in (py-configparser:sections config)
appending (loop for i in (py-configparser:items config s)
collect (make-config-accessor s (car i)))))
然后,例如如果 /tmp/x.ini
包含
[Settings]
db = "test.db"
scrunge = 12
然后
> (gen-accessors (read-config "/tmp/x.ini"))
(get-scrunge get-db)
> (get-scrunge (read-config "/tmp/x.ini"))
"12"
你可以用这样的方式定义 make-config-accessor
甚至更好:
(defun curryr (f &rest trailing-args)
(lambda (&rest args)
(declare (dynamic-extent args))
(apply f (append args trailing-args))))
(defun make-config-accessor (section option)
;; Make an accessor for OPTION in SECTION with a suitable name
(let ((fun-name (intern (nsubstitute #\- #\_
(format nil "GET-~A"
(string-upcase option))))))
(setf (symbol-function fun-name)
(curryr #'py-configparser:get-option section option))
fun-name))
当然,并不是每个人都会觉得这更好。