为什么CLISP无法使用未加工名称调用某些函数?

时间:2013-10-03 13:44:18

标签: lisp common-lisp sbcl clisp

我编写了一个ad hoc解析器生成器,它创建代码将旧的和鲜为人知的7位字符集转换为unicode。对解析器生成器的调用扩展为包含在defun中的一堆progn,然后进行编译。我只想将生成的defun之一(顶级的)暴露给系统的其余部分;所有其他内容都是解析器的内部,只能从顶层的动态范围内调用。因此,生成的其他defun具有未分隔的名称(使用gensym创建)。这个策略适用于SBCL,但我最近使用CLISP进行了第一次测试,我得到的错误如下:

*** - FUNCALL: undefined function #:G16985

似乎CLISP无法处理具有未分隔名称的函数。 (有趣的是,系统编译没有问题。)编辑:在大多数情况下,似乎它可以处理具有未分隔名称的函数。请参阅下面的Rörd的答案。

我的问题是:这是CLISP的一个问题,还是某些实现(例如SBCL)碰巧克服了Common Lisp的限制?

修改

例如,顶级生成函数(名为parse)的宏扩展的表达式如下:

(PRINC (#:G75735 #:G75731 #:G75733 #:G75734) #:G75732)

评估此表达式(通过调用parse)会导致类似上面的错误,即使该函数在同一个宏扩展中明确定义:

(DEFUN #:G75735 (#:G75742 #:G75743 #:G75744) (DECLARE (OPTIMIZE (DEBUG 2)))
 (DECLARE (LEXER #:G75742) (CONS #:G75743 #:G75744))
 (MULTIPLE-VALUE-BIND (#:G75745 #:G75746) (POP-TOKEN #:G75742)
 ...

#:G75735的两个实例绝对是相同的符号 - 而不是两个具有相同名称的不同符号。正如我所说,这适用于SBCL,但不适用于CLISP。

修改

SO用户Joshua Taylor指出这是由于a long standing CLISP bug

5 个答案:

答案 0 :(得分:5)

你没有显示给出错误的一行,所以我只能猜测,但就我所见,唯一可能导致这个问题的是你指的是符号的名称尝试调用它时,而不是符号本身。

如果您指的是符号本身,那么您的所有lisp实现都必须查找该符号的symbol-function。是否实习并不重要。

请问为什么你没有考虑另一种隐藏函数的方法,即labels语句或定义只导出一个外部函数的新包中的函数?

编辑:以下示例将从与CLISP提示的交互中逐字复制。

正如您所看到的,调用gensym命名的函数正在按预期工作。

[1]> (defmacro test ()
(let ((name (gensym)))
`(progn
(defun ,name () (format t "Hello!"))
(,name))))
TEST
[2]> (test)
Hello!
NIL

也许你试图调用函数的代码在defun之前得到了评估?如果宏扩展中除了各种defun之外还有任何代码,它可能是依赖于实现的,首先得到的是什么,因此SBCL和CLISP的行为可能会有所不同,而且没有任何违反标准的行为。

编辑2 :一些进一步的调查表明,CLISP的行为取决于代码是直接解释还是首先编译然后解释。您可以通过直接load CLISP中的Lisp文件或首先在其上调用compile-file然后load FASL来查看差异。

通过查看CLISP提供的第一次重启,您可以看到正在发生的事情。它说的是“输入要使用的值而不是(FDEFINITION'#:G3219)。”因此,对于已编译的代码,CLISP引用符号并按名称引用它。

看起来这种行为符合标准。可以在HyperSpec中找到以下定义:

  

函数指示符 n。函数的指示符;也就是说,一个表示函数的对象,它是以下之一:符号(表示在全局环境中由该符号命名的函数)或函数(表示自身)。如果符号用作函数指示符但是它没有作为函数的全局定义,或者它具有作为宏或特殊形式的全局定义,则后果是未定义的。另请参见扩展函数指示符。

我认为一个未加工的符号与“一个符号被用作函数指示符但它没有一个全局定义作为一个函数”的情况相匹配。对于未指明的后果。

编辑3 :(我同意我不确定CLISP的行为是否是一个错误。对标准术语的细节更有经验的人应该判断这个。这取决于是否一个未加工符号的功能单元 - 即一个不能通过名称引用的符号,只能通过直接保持符号对象 - 将被视为“全局定义”而不是)

无论如何,这是一个解决CLISP问题的示例解决方案,通过实现一次性包中的符号来解决问题,避免了未加工符号的问题:

(defmacro test ()
  (let* ((pkg (make-package (gensym)))
         (name (intern (symbol-name (gensym)) pkg)))
    `(progn
       (defun ,name () (format t "Hello!"))
       (,name))))

(test)

编辑4 :正如Joshua Taylor在对该问题的评论中指出的那样,这似乎是(10岁)CLISP bug #180的情况。

我已经测试了该错误报告中建议的两种解决方法,并发现用progn替换locally实际上没有帮助,但用let ()替换它。

答案 1 :(得分:2)

您当然可以定义名称为未分隔符号的函数。例如:

CL-USER> (defun #:foo (x)
           (list x))
#:FOO
CL-USER> (defparameter *name-of-function* *)
*NAME-OF-FUNCTION*
CL-USER> *name-of-function*
#:FOO
CL-USER> (funcall *name-of-function* 3)
(3)

但是,每次读取这样的表单时,sharpsign colon语法都会引入一个新符号:

  

#:引入一个名称为symbol-name的未分隔符号。每次遇到此语法时,都会创建一个不同的未处理符号。 symbol-name必须具有没有包前缀的符号的语法。

这意味着即使像

这样的东西
CL-USER> (list '#:foo '#:foo)
;=> (#:FOO #:FOO) 

显示相同的打印表示,您实际上有两个不同的符号,如下所示:

CL-USER> (eq '#:foo '#:foo)
NIL

这意味着如果您尝试通过键入#:来调用此类函数,然后输入命名该函数的符号名称,那么您将遇到麻烦:

CL-USER> (#:foo 3)
; undefined function #:foo error

所以,虽然您可以使用类似我给出的第一个示例来调用该函数,但最后一个不能这样做。这可能会让人感到困惑,因为打印的表示使它看起来像是正在发生的事情。例如,您可以编写如下的这样的阶乘函数:

(defun #1=#:fact (n &optional (acc 1))
  (if (zerop n) acc
      (#1# (1- n) (* acc n))))

使用特殊的读者符号#1=#:fact#1#以后引用相同的符号。但是,看看打印相同表单时会发生什么:

CL-USER> (pprint '(defun #1=#:fact (n &optional (acc 1))
                    (if (zerop n) acc
                        (#1# (1- n) (* acc n)))))

(DEFUN #:FACT (N &OPTIONAL (ACC 1))
  (IF (ZEROP N)
      ACC
      (#:FACT (1- N) (* ACC N))))

如果您使用该打印输出,并尝试将其复制并粘贴为定义,那么当涉及#:FACT的两次出现时,读者会创建名为“FACT”的两个符号,该功能不起作用(你甚至可能得到未定义的功能警告):

CL-USER> (DEFUN #:FACT (N &OPTIONAL (ACC 1))
           (IF (ZEROP N)
               ACC
               (#:FACT (1- N) (* ACC N))))

; in: DEFUN #:FACT
;     (#:FACT (1- N) (* ACC N))
; 
; caught STYLE-WARNING:
;   undefined function: #:FACT
; 
; compilation unit finished
;   Undefined function:
;     #:FACT
;   caught 1 STYLE-WARNING condition

答案 2 :(得分:0)

我希望我能解决问题。对我来说,它适用于CLISP。

我尝试过这样的方法:使用宏来创建具有GENSYM-ed名称的函数。

(defmacro test ()  
  (let ((name (gensym)))  
    `(progn  
       (defun ,name (x) (* x x))  
       ',name)))

现在我可以获得名称(setf x (test))并将其命名为(funcall x 2)

答案 3 :(得分:0)

是的,定义具有无意义符号名称的函数是完全正确的。问题是你不能再“按名称”调用它们,因为你不能按名称获取未加工的符号(这实际上是“未加工”的意思)。

您需要将未分隔符号存储在某种数据结构中,然后才能获取符号。或者,将定义的函数存储在某种数据结构中。

答案 4 :(得分:0)

令人惊讶的是,CLISP bug 180实际上并不是ANSI CL一致性错误。不仅如此,但显然,ANSI Common Lisp在这方面本身就是如此突破,即使基于progn的解决方案也是对实现的礼貌。

Common Lisp是一种用于编译的语言,编译会产生有关对象标识的问题,这些对象被放入已编译的文件中并在以后加载("外化"对象)。 ANSI Common Lisp要求从编译文件中再现的文字对象仅与原始对象相似。 (CLHS 3.2.4 Literal Objects in Compiled Files)。

首先,根据定义相似性(3.2.4.2.2 Definition of Similarity),未加工符号的规则是相似性是基于名称的。如果我们使用包含未处理符号的文字编译代码,那么当我们加载编译文件时,我们会得到一个类似的符号而不是(必然)相同的对象:具有相同名称的符号

如果将相同的未处理符号插入到两个不同的顶级表单中然后编译为文件,该怎么办?加载文件时,这两个文件是否至少类似于彼此?不,没有这样的要求。

但情况变得更糟:也没有要求同一形式中同一个未处理符号的两次出现将以保持其相对身份的方式外部化:该对象的加载版本将在原始所有位置具有相同的符号对象。事实上,相似性的定义不包含保留循环结构和子结构共享的条款。如果我们有像'#1=(a b . #1#)这样的文字,作为编译文件中的文字,似乎没有要求将其作为具有与原始图形结构相同的圆形对象(图形同构)再现。 conses的相似性规则以天真递归的形式给出:如果它们各自的carcdr s相似,则两个conses相似。 (规则甚至不能评估圆形对象;它不会终止)。

上述工作是因为实施超出了规范要求;他们提供的扩展名符合(3.2.4.3 Extensions to Similarity Rules)。

因此,纯粹根据ANSI CL,我们不能指望在编译文件中使用带有gensyms的宏,至少在某些方面。在下面的代码中表达的期望违反了规范:

(defmacro foo (arg)
   (let ((g (gensym))
         (literal '(blah ,g ,g ,arg)))
      ...))

(defun bar ()
  (foo 42))

bar函数包含一个带有两个gensym插入的文字,根据conses和symbols的相似性规则,它不需要再现为包含第二和第三个位置中同一个对象的两次出现的列表。

如果上述方法按预期工作,则由于相似性规则的扩展而导致#34;

所以答案是"为什么不能CLISP ......"问题是虽然CLISP确实提供了相似性的扩展,它保留了文字形式的图形结构,但它并不是在整个编译文件中进行,只是在该文件中的单个顶级项目中。 (它使用*print-circle*来发出单个项目。)错误是CLISP不符合用户可以想象的最佳行为,或者至少符合其他实现所表现出的更好的行为。