在设计宏之类的东西时(请记住,在设计宏时,您是在设计一种编程语言)需要考虑的一件事是人们期望如何阅读该语言。如果您正在设计的编程语言是 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 中已经存在一个很好的例子:defstruct。 defstruct有两种基本情况:
(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))))
...))
顺便说一句,值得考虑为什么 defclass 和 defstruct 做不同的事情,以及这对其他宏意味着什么。
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。如果我设计了这个,我会做的稍微不同!