首页 > 解决方案 > 有没有办法混合 LISP 的宏 &optional 和 &key 参数?

问题描述

我想定义一个 LISP 宏,这样dolist我就可以定义一个可选的输出参数。在以下案例研究中,将调用此宏doread。它将从文件中读取行并返回以这种方式找到的行数。

(let ((lines 0))
  (doread (line file lines)
     ;; do something with line
     (incf lines)))

问题是让它lines在上面的宏中工作

我可以用 &key 做我想做的事,但不能用 &optional &key (并且 &key 是必需的,因为我想控制文件的读取方式;例如使用readorread-line或其他)。

现在以下工作以错误的方式工作。这里的out参数必须是 a&key而不是 &optional:

;; this way works... 

(defmacro doread ((it f  &key out (take #'read)) &body body)
  "Iterator for running over files or strings."
  (let ((str (gensym)))
    `(with-open-file (,str f)
       (loop for ,it = (funcall ,take ,str nil)
             while ,it do
             (progn ,@body))
       ,out)))

;; lets me define something that reads first line of a file
(defun para1 (f)
  "Read everything up to first blank line."
  (with-output-to-string (s)
    (doread (x f :take #'read-line)
      (if (equalp "" (string-trim '(#\Space #\Tab) x))
        (return)
        (format s "~a~%" x)))))

(print (para1 sometime)) ; ==> shows all up to first blank line

我想做的是以下内容(注意out现在已移至&optional

(defmacro doread ((it f &optional out &key   (take #'read)) &body body)
  "Iterator for running over files or strings."
  (let ((str (gensym)))
    `(with-open-file (,str f)
       (loop for ,it = (funcall ,take ,str nil)
             while ,it do
             (progn ,@body))
       ,out)))

如果可行,我可以做类似的事情。

(defun para1 (f)
  "Print  everything up to first blank line. 
   Return the lines found in that way"
  (let ((lines 0))
      (doread (x f lines :take #'read-line)
        (if (equalp "" (string-trim '(#\Space #\Tab) x))
            (return)
            (and (incf lines) (format t "~a~%" x)))))

但我用&optional out我得到

 loading /Users/timm/gits/timm/lisp/src/lib/macros.lisp
*** - GETF: the property list (#'READ-LINE) has an odd length

标签: macroslispcommon-lisp

解决方案


您不能混合&optional&key期望只能传递关键字参数。但是,您可以定义允许与源关联的可选参数列表的语法。

例如:

(defpackage :so (:use :cl :alexandria))
(in-package :so)

(defmacro do-read ((entry source &optional result) &body body)
  (destructuring-bind (source &key (take '#'read)) (ensure-list source)
    (once-only (take)
      `(loop
          :with ,entry
          :do (setf ,entry (handler-case (funcall ,take ,source)
                             (end-of-file () (loop-finish))))
            (progn ,@body)
          :finally (return ,result)))))

的语法DO-READ可以写成:

(DO-READ (ENTRY [SOURCE|(SOURCE &KEY TAKE)] &OPTIONAL RESULT) . BODY)

这不是标准 Lisp 形式的不寻常语法(请参阅LETlambda-lists 中的关键字 synaxdefstruct等)。您可以添加更多关键字参数以及TAKE.

评论

  • 在宏中,我更喜欢发出 LOOP 关键字作为关键字,而不是宏定义包中的符号;否则,在对代码进行宏扩展时,您可能会得到以宏包为前缀的符号(即,SO::WITH而不是:WITH),这很快就会变得不可读。

  • 返回 NIL fromREAD-LINE很好,但不是 from READ,因为 NIL 可能是成功读取的值。一般来说,既然TAKE是用户提供的,你不知道 NIL 是否是一个可接受的结果。这就是为什么我抓住 END-OF-FILE了。如果您想从其他来源读取信息,您还可以检查辅助返回值,或记录它们也表示条件。

  • 变量的ENTRY范围被扩展,因此RESULT可以是 ENTRY它自己;在您的情况下,OUT不能等于IT,因为一旦退出循环,您将无法再访问它。这是一个小问题,但这可能很有用。

  • 我没有包括WITH-OPEN-FILE,以防您想从文件(流)以外的其他内容中读取。

  • #'READ引用,这在这里并不重要,而是在宏中养成的一个好习惯,以便您在评估时实际评估事物,而不是在宏扩展时。

例子

(with-input-from-string (in "abcdef")
  (do-read (char (in :take #'read-char) char)
    (print char)))

打印所有字符并返回#\f

(with-input-from-string (in (format nil "~{~a~%~}" *features*))
  (let ((lines 0))
    (do-read (line in lines)
      (incf lines))))

打印字符串中的行数。


推荐阅读