我到底是怎么把HyperSpec的文档字符串规范读错的?
表面上看,SBCL、Clozure Common Lisp和 CLISP已经实现了 defun,使得在某些情况下文档字符串没有被正确处理。是我对HyperSpec的解读有误(很可能),HyperSpec本身就有缺陷,还是这三个实现以同样的方式出了错?
HyperSpec的说法
defun function-name lambda-list [[declaration* | documentation]] form*
1.4.1.2.1节描述了在修改后的BNF语法中用于描述Common Lisp语法的拼接表达式。还提供了一些示例:
例如,表达式
(x [[A | B* | C]] y)
意味着在任意顺序中最多出现一个A、任意数量的B,以及最多一个C。它描述了以下任意一种情况:
(x y)
(x B A C y)
(x A B B B B B C y)
(x C B A B B B y)
我对HyperSpec的解读
从 defun 所描述的语法以及我对修改后的BNF语法的理解来看,lambda-list之后似乎可以跟随任意数量的声明、至多一个文档字符串,以及任意数量的主体形式。
这让我预计可以用如下方式定义一个带有文档字符串但没有实现体的函数占位符(stub):
(defun func-a ()
"This is FUNC-A.")
但这会带来问题:有时你可能想定义一个仅返回字符串的函数。实现应该如何区分文档字符串和应返回的字符串?这似乎是HyperSpec的潜在缺陷。
实际发生了什么
给定以下定义:
(defun func-a ()
"This is FUNC-A.")
(defun func-b ()
"This is FUNC-B"
nil)
以下是一个典型的REPL交互:
CL-USER> (documentation 'func-a 'function)
NIL
CL-USER> (documentation 'func-b 'function)
"This is FUNC-B"
CL-USER> (func-a)
"This is FUNC-A."
CL-USER> (func-b)
NIL
CL-USER> (macroexpand-1 '(defun func-a () "This is FUNC-A."))
(PROGN
(EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-C:%COMPILER-DEFUN 'FUNC-A T NIL NIL))
(SB-IMPL::%DEFUN 'FUNC-A
(SB-INT:NAMED-LAMBDA FUNC-A
NIL
(BLOCK FUNC-A "This is FUNC-A."))))
T
CL-USER> (macroexpand-1 '(defun func-b () "This is FUNC-B." nil))
(PROGN
(EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-C:%COMPILER-DEFUN 'FUNC-B T NIL NIL))
(SB-IMPL::%DEFUN 'FUNC-B
(SB-INT:NAMED-LAMBDA FUNC-B
NIL
"This is FUNC-B."
(BLOCK FUNC-B NIL))))
T
似乎对于 func-a,文档字符串被捕获在函数的主体形式中;而对于 func-b,在文档字符串之后加入一个 nil 表达式(也就是加入一个单独的主体形式)使实现能够将文档字符串作为文档字符串来处理。换句话说,至少对这些实现(SBCL、CCL和 CLISP)来说,除非包含文档字符串,否则需要零个或多个主体形式;若包含文档字符串,则需要一个或多个主体形式。我还没有尝试添加declarations。
上述交互是在SBCL中完成的。CCL和 CLISP的行为相同。CCL与 CLISP的宏展开不同,但两者在函数的主体形式中都以类似的方式捕获文档字符串。
我知道实现对处理文档字符串的方式有一定的自由度。也许我测试的这三种Common Lisp实现都利用了这种自由度,以确保像 func-a 这样的简单函数总是返回它们的字符串,从而解决标准中的潜在缺陷。
问题到底是什么?
这里到底发生了什么?这是标准中一个(显然众所周知的)缺陷导致这三种实现以同样的方式解决,还是说我对相关规范以某种方式理解错了?
解决方案
据我所知,你的担忧在 3.4.11 Syntactic Interaction of Documentation Strings and Declarations 注释中得到了回答:
if a string S appears where a documentation string is permissible and is not followed by either a declare expression or a form then S is taken to be a form; otherwise, S is taken as a documentation string.