Lisp - 参数列表中间的可选参数

Lisp - optional argument in the middle of parameter list

我目前有一个定义如下的宏:

(defmacro some-macro (generic-name (&rest args) &body body) 
  ...)

我现在想给这个宏添加一个额外的参数,一种用户可以选择提供的标志,它会改变宏的行为。下面的代码应该是可能的:

(some-macro some-name (arg1 arg2) (print (+ arg1 arg2)))
(some-macro :flag some-name (arg1 arg2) (print (special-+ arg1 arg2)))

我故意只是做了一个概念性的例子来关注重要的事情而不是我试图实现的真正的宏。真正的宏是不同的并且更复杂。如您所见,现在应该可以使用 :flag 参数(flag 可以是任何单词,只要其前缀为“:”)和不使用 flag 参数来调用宏。有没有办法在不将 &optional 关键字定位在参数列表末尾的情况下执行此操作(即它确实需要位于第一个位置)。

&optional 只能在位置参数的末尾(后面可以有 &rest&body)。即使你可以把它放在前面,如果还有 &rest&body,它怎么知道你是否提供了可选参数?例如。如果 lambda 列表是

(&optional arg1 arg2 &rest rest-args)

电话是

(func-name 1 2 3 4 5)

它可以是 arg1 = 1, arg2 = 2, rest-args = (3 4 5)arg1 = NIL, arg2 = 1, rest-args = (2 3 4 5)

您需要定义宏以采用单个 &rest 参数。然后你可以检查第一个参数是否是关键字,更新参数列表以添加默认值,然后用 destructuring-bind.

解析它
(defmacro some-macro (&rest all-args) 
  (unless (keywordp (first all-args))
    (push nil all-args)) ;; flag defaults to NIL
  (destructuring-bind (flag generic-name (&rest args) &body body) all-args
    ...))
(some-macro :flag some-name (arg1 arg2) (print (special-+ arg1 arg2)))

(some-macro some-name (arg1 arg2) (print (+ arg1 arg2)))

只能猜测什么有用,因为宏的设计更多地取决于上下文:它实际用于什么。

例如在CLOS中我们写

(defmethod foo :around ((a class-a)) ...)

首先是名称,然后是零个或多个方法限定符(此处为 :around,然后是 arglist。在典型的定义宏中(那些以 def).

为此,我们需要自己编写 arglist 的宏解构,因为它不匹配内置的宏 arglist 模式。

(defmacro some-macro (name &rest args)
  (let* ((qualifiers (loop for arg in args
                           until (listp arg)
                           collect (pop args)))
         (arg-list (pop args))
         (body args))

    ... ; return something for the example

    ))

在其他宏中我们可能会这样写

(some-macro some-name (arg1 arg2 :flag)
  (print (special-+ arg1 arg2)))

(defmacro some-macro (some-name (arg1 arg2 &optional flag) &body body)
  ...)

类似于

(with-input-from-string (stream string :start 10)
  (... ))

尽管上面使用了关键字,而不是选项。

或者我们可能想写:

(some-macro some-name (arg1 arg2) (:flag1)
  (print (special-+ arg1 arg2)))

(defmacro some-macro (some-name (&rest args) (&rest flags) &body body)
  ...)

如果只有三个标志,也可以生成三个不同名称的不同宏并删除标志。

在设计宏之类的东西时要考虑的一件事(记住,当你设计宏时,你是在设计一种编程语言)是人们期望如何阅读该语言。如果您正在设计的编程语言是 CL 的轻微超集,或者在其他方面非常接近 CL,那么您可能希望不违反 CL 程序员在阅读 CL 代码时的期望,或者更一般地说,Lisp 程序员在阅读 Lisp 代码时的期望.

[请注意,以下大部分是意见:人们显然有不同的意见——这些是我的,他们并不比其他人的更正确。]

那么,这些期望是什么?那么,它们可能包括以下两个:

  • 人们从左到右阅读 CL,因此表格左端的内容往往在视觉上更重要;
  • CL 中的许多现有表单看起来像 (<operator> <thing> ...)表单中的前两个子表单 是迄今为止最有趣的,第二个通常比首先。想想 (defun foo ...), (dolist (x ...) ...), (let ((x y) ...) ...).

违反这些期望的一个例子是一个对象系统,它使用一些 send 操作明确地与消息传递一起工作(我认为 Old Flavors 这样做了,我认为 New Flavors 没有,但我现在的记忆很模糊)。使用这些编写的代码看起来像 (send <object> <message> ...):许多形式的第一个单词是 send。这意味着这个用于阅读代码的视觉上重要的地方已经完全浪费了,因为它总是同一个词,而重要的地方现在是第二个和第三个子表单。好吧,相反,我们可以省略整个 send 并写成 (message object ...)(object message ...)。 CLOS 基本上采用了这些选项中的第一个,其中 'message' 是一个通用函数,当然通用函数可以专注于多个参数,这打破了整个消息传递范式。但是您可以像编写消息传递一样编写 CLOS,并且它可以工作,这意味着它与许多其他 CL 代码的外观一致。

那么,好吧,让我们看一下您的宏的两种情况:

(some-macro some-name ...)

这很好。

(some-macro :flag some-name ...)

但这已经用一些与宏无关的东西填充了视觉第二位置:它只是一些可选参数。有趣的是现在第三个位置。

嗯,我们该如何解决这个问题?事实证明,CL 中已经存在一个很好的示例:defstructdefstruct 有两种基本情况:

(defstruct structure-name
  ...)

(defstruct (structure-name ...)
  ...)

这两个都满足前两个位置最重要的要求,同时允许可选参数并在使用时清楚地直观指示。

(旁白:defclasss 通过将选项放在末尾来做不同的事情:

(defclass name (...supers...)
  (...slot specifications...)
  ...options...))

我觉得哪个都可以。)

因此,重做宏的一种方法是 defstruct。在这种情况下,您将拥有

之一
(some-macro some-name (...)
  ...)

(some-macro (some-name :flag) (...)
  ...)

而且您可以很容易地实现它:

(defmacro some-macro (thing (&rest args) &body forms)
  (multiple-value-bind (the-thing the-options)
      (etypecase thing
        (symbol (values thing '()))
        (cons
         (destructuring-bind (proposed-name . proposed-options) thing
           (unless (symbolp proposed-name)
             (error ...))
           (unless (proper-list-p proposed-options)
             (error ...))
           (values proposed-name proposed-options))))
    ...))

事实上,我会更进一步:人们期望关键字参数具有值,因为在大多数其他地方他们这样做。所以取而代之的是

(some-macro (some-name :flag t) (...)
  ...)

这符合预期。这有一个额外的好处,你可以只使用 CL 的参数解析来获取信息:

> (destructuring-bind (&key (flag nil flagp)) '(:flag t)
    (values flag flagp))
t
t

比如。如果您像这样编写宏,您可能会得到如下所示的内容:

(defmacro some-macro (thing (&rest args) &body forms)
  (multiple-value-bind (the-thing flag flagp)
      (etypecase thing
        (symbol (values thing nil nil))
        (cons
         (destructuring-bind (proposed-name (&key (flag nil flagp))) thing
           (unless (symbolp proposed-name)
             (error ...))
           (values proposed-name flag flagp))))
    ...))

顺便说一句,值得考虑为什么 defclassdefstruct 做事不同,以及这对其他宏意味着什么。

defstruct 看起来像

(defstruct structure-name-and-options
  slot-description
  ...)

这意味着,如果您将与结构本身关联的选项放在末尾,它们将与插槽描述混淆。

defclass 看起来像这样解决这个问题:

(defclass class-name (superclass-name ...)
  (slot-description
   ...)
  [class-option] ...)

它将插槽描述嵌套在另一个列表中,这意味着现在在表单末尾的 class 选项的模式中有 space。

对于具有某种 'body' 的宏,那么自然模式看起来像

(with-foo something [some-more-special-things] ...
  form
  ...)

例如

(with-slots (sa sb) x
  (when (> sa sb)
    (setf sb (+ sa sb)))
  (values sa sb))

这里的问题是宏表单的整个尾部都是主体,这意味着末尾没有自然的选项位置:唯一可以放置它们的地方是开头的某个地方。你可以再次通过嵌套主体来解决这个问题:

(with-weird-thing x (y z)
  ((when y
     ...)
   (print z)
   ...)
  option ...)

但这又一次违背了人们的期望:没有(?)标准的 CL 宏可以做到这一点。重要的是 defclass 的 'body' 不是某种形式:它是一个插槽规范列表。所以defclass.

采用这种模式是合理的

最后值得考虑defmethod。如果我设计了这个,我会做的略有不同!

严肃的方法:将带有标志的宏语法重写为另一个宏,该宏采用包含标志的固定参数:

(defmacro some-macro-w-flags (flags name (&rest args) &body body)
   ...)

(defmacro some-macro (&rest args)
  (let ((flags))
    (loop while (keywordp (car args))
          do (push (pop args) flags))
    `(some-macro-w-flags ,flags ,@args)))

一些测试:

[1]> (macroexpand-1 '(some-macro abc (1 2 3)))
(SOME-MACRO-W-FLAGS NIL ABC (1 2 3)) ;
T
[2]> (macroexpand-1 '(some-macro :foo abc (1 2 3)))
(SOME-MACRO-W-FLAGS (:FOO) ABC (1 2 3)) ;
T
[3]> (macroexpand-1 '(some-macro :foo :bar abc (1 2 3)))
(SOME-MACRO-W-FLAGS (:BAR :FOO) ABC (1 2 3)) ;
T