评估传递给在 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

注意,因为你需要一个函数,所以你必须在调用中引用参数 configsection(它们在函数中等待评估 option得到评估。

感谢 quotebackquote 以及 unquoteeval,您可以完全控制 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)))))

现在 optionlet* 形式计算。 然后需要使用 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))

当然,并不是每个人都会觉得这更好。