【问题标题】:Is there a way to mix a LISP's macro &optional and &key params?有没有办法混合 LISP 的宏 &optional 和 &key 参数?
【发布时间】:2019-06-26 15:42:10
【问题描述】:

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

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

问题是让lines 在上述宏中工作

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

现在以下工作以错误的方式工作。这里out 参数必须是&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

【问题讨论】:

  • 请注意,在 DOREAD 中,f 必须是 ,f

标签: macros lisp common-lisp


【解决方案1】:

您不能混合使用 &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)

这不是一个不寻常的语法 w.r.t。标准 Lisp 形式(参见LET、lambda 列表中的关键字 synax、defstruct 等)。 您可以添加更多关键字参数以及TAKE

备注

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

  • READ-LINE 返回 NIL 很好,但从 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))))

打印字符串中的行数。

【讨论】:

  • 这太好了,谢谢。 destructuring-bind?很酷。没想到用那个。但是once-only 让我很困惑。和其他人。 common-lisp.net/project/cl-utilities/doc/once-only.html你能澄清一下吗?
  • once-only 就像 with-gensyms 一样,对于宏观卫生也是如此。但确保由 gensym - 替换的变量将在宏体中仅执行一次。所以一个非常实用的宏——如果使用with-gensyms,大多数人想要once-only
  • 这里对once-only有一个非常好的深入的逐步解释:malisper.me/once-only
  • '你不能混合 &optional 和 &key' ?
  • 请注意(尽管这可能无法在这里工作,因为您不知道take 是什么以及它的约定是什么)read 的一个很好的 EOF 检测技巧和兼容的函数参数是(eql (read in nil in) in),因为它们永远无法返回的一个值是它们正在读取的流。这避免了潜在的代价高昂的异常。
【解决方案2】:

为我工作:

(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))))))

在 LispWorks 中使用它:

CL-USER 104 > (para1 (capi:prompt-for-file "text file"))
;;; -*- mode: Lisp; Base: 10 ; Syntax: ANSI-Common-Lisp ; buffer-read-only: t; -*-
;;; This is ASDF 3.3.3: Another System Definition Facility.
;;;
;;; Feedback, bug reports, and patches are all welcome:
;;; please mail to <asdf-devel@common-lisp.net>.
;;; Note first that the canonical source for ASDF is presently
;;; <URL:http://common-lisp.net/project/asdf/>.
;;;
;;; If you obtained this copy from anywhere else, and you experience
;;; trouble using it, or find bugs, you may want to check at the
;;; location above for a more recent version (and for documentation
;;; and test files, if your copy came without them) before reporting
;;; bugs.  There are usually two "supported" revisions - the git master
;;; branch is the latest development version, whereas the git release
;;; branch may be slightly older but is considered `stable'
15

只是一旦你想指定:take,你还需要提供可选的arg。这是一个常见的陷阱,这就是为什么不喜欢可选参数和关键字参数的组合。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-30
    • 2013-04-16
    • 2022-01-01
    • 2022-06-10
    相关资源
    最近更新 更多