【问题标题】:Lisp - optional argument in the middle of parameter listLisp - 参数列表中间的可选参数
【发布时间】:2019-12-31 00:51:42
【问题描述】:

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

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

我特意只是做了一个概念性的例子来关注什么是重要的,而不是我想要实现的真正的宏。真正的 a 宏是不同的并且更复杂。如您所见,宏现在应该可以使用 :flag 参数(flag 可以是任何单词,只要它的前缀是 ':')和没有 flag 参数即可调用。有没有办法做到这一点,而无需将&optional 关键字定位在参数列表的末尾(即它确实需要位于第一个位置)。

【问题讨论】:

  • 如果要这样做,必须自己做参数解析,不能使用内置解构。
  • :flag 应该是generic-name 吗?如果没有,你的电话在哪里?
  • :flag 可以在我的代码中采用三个 from 之一,我们称之为 :flag1:flag2:flag3。根据标志或没有标志,宏以特定方式展开。我对generic-name 并不完全熟悉,如果您指的是特定的东西,但希望这能解决一些问题。
  • 这是宏的第一个参数:(defmacro some-macro (generic-name ...))
  • 如果generic-name 是我的宏的第一个参数的同义词,那么是的,它就是generic-name。但是,它是可选的。不提供标志也应该起作用。应该如何调用宏的确切语法在问题的第二个代码块中。

标签: macros lisp common-lisp


【解决方案1】:

&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
    ...))

【讨论】:

  • 您不需要pop :keyword 参数来使all-args 将关键字值作为第一个元素吗?
  • 关键字不是用来引入另一个参数的,它本身就是参数。
【解决方案2】:
(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)
  ...)

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

【讨论】:

  • 您能否详细说明您将如何处理第一种情况(CLOS 情况)。现在我想起来,首先命名实际上会更理想,而我想要实现的目标与该示例非常匹配(事实上,这几乎正是我想要模仿的)。
【解决方案3】:

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

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

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

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

一个违反这些期望的例子是一个对象系统,它使用一些send 操作显式处理消息传递(我认为旧风味做到了这一点,我认为新风味没有做到这一点,但我的记忆现在很模糊)。使用这些编写的代码看起来像(send <object> <message> ...):许多形式的第一个单词是send。这意味着这个阅读代码的视觉上重要的地方已经被完全浪费了,因为它总是同一个词,重要的地方现在是第二个和第三个子形式。好吧,相反,我们可以省略整个 send 并写成 (message object ...)(object message ...)。 CLOS 基本上采用了这些选项中的第一个,其中“消息”是一个泛型函数,当然泛型函数可以专注于多个参数,这会破坏整个消息传递范式。但是您可以编写 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] ...)

它已将插槽描述嵌套在另一个列表中,这意味着模式中现在有空间用于表单末尾的类选项。

对于具有某种“主体”的宏,自然模式看起来像

(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 的“正文”不是某种形式:它是插槽规范的列表。所以defclass采用这种模式是合理的。

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

【讨论】:

    【解决方案4】:

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

    (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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-19
      • 1970-01-01
      • 2015-07-28
      • 2019-11-09
      • 1970-01-01
      • 2013-02-28
      • 2011-08-04
      • 1970-01-01
      相关资源
      最近更新 更多