4

我编写了一个临时解析器生成器,它创建代码以将旧的和鲜为人知的 7 位字符集转换为 unicode。对解析器生成器的调用扩展为包含在 a 中的一堆defuns progn,然后被编译。我只想将生成defun的 s 之一——顶层的一个——暴露给系统的其余部分;所有其他都是解析器内部的,并且只能从顶级解析器的动态范围内调用。因此,defun生成的其他 s 具有非内部名称(使用 创建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 指出这是由于长期存在的 CLISP 错误

4

5 回答 5

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? 如果宏扩展中除了各种defuns 之外还有任何代码,那么首先评估的内容可能取决于实现,因此 SBCL 和 CLISP 的行为可能会有所不同,但它们中的任何一个都不会违反标准。

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

您可以通过查看 CLISP 提供的第一次重启来了解发生了什么。它说类似“输入要使用的值而不是 (FDEFINITION '#:G3219)”。因此,对于编译后的代码,CLISP 引用符号并按名称引用它。

尽管这种行为似乎符合标准。在 HyperSpec 中可以找到以下定义:

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

我认为 uninterned 符号与“符号用作函数指示符但它没有作为函数的全局定义”的情况相匹配,以获得未指定的后果。

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

于 2013-10-03T14:54:57.357 回答
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 冒号语法都会引入一个新符号:

#:引入了一个名为 symbol-name 的 uninterned 符号。每次遇到这种语法时,都会创建一个不同的非内部符号。符号名必须具有不带包前缀的符号语法。

这意味着即使像

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”的符号,并且该函数将不起作用(您甚至可能会得到 undefined功能警告):

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
于 2013-10-03T15:37:24.757 回答
0

是的,定义具有非预期符号名称的函数是非常好的。问题是您不能“按名称”调用它们,因为您无法按名称获取未实习的符号(本质上,这就是“未实习”的意思)。

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

于 2013-10-03T15:44:55.467 回答
0

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

我是这样尝试的:使用宏来创建具有 GENSYM 名称的函数。

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

现在我可以得到名字(setf x (test))并调用它(funcall x 2)

于 2013-10-03T14:29:32.970 回答
0

令人惊讶的是,CLISP 错误 180实际上并不是一个 ANSI CL 一致性错误。不仅如此,而且很明显,ANSI Common Lisp 本身在这方面已经很糟糕,以至于即使是progn基于解决方案的解决方法也是实现的礼貌。

Common Lisp 是一种用于编译的语言,编译会产生关于对象身份的问题,这些对象被放入编译文件并随后加载(“外部化”对象)。ANSI Common Lisp 要求从编译文件复制的文字对象仅与原始对象相似。CLHS 3.2.4编译文件中的文字对象)。

首先,根据相似度的定义(3.2.4.2.2 相似度的定义),无间符号的规则是相似度是基于名称的。如果我们使用包含非驻留符号的文字编译代码,那么当我们加载编译后的文件时,我们会得到一个相似的符号,而不是(必然)相同的对象:具有相同名称的符号。

如果将相同的非内部符号插入到两个不同的顶级表单中,然后编译为文件怎么办?加载文件时,这两个至少彼此相似吗?不,没有这样的要求。

但情况变得更糟:同样没有要求以相同形式出现的相同非内部符号的两次出现将以保留它们的相对身份的方式外部化:该对象的重新加载版本将具有相同的符号对象在原件所在的所有地方。事实上,相似性的定义并没有规定保持循环结构和子结构共享。如果我们有一个像'#1=(a b . #1#), 作为编译文件中的字面量的字面量,似乎没有要求将其复制为具有与原始图结构相同的图结构的圆形对象(图同构)。conses 的相似性规则以朴素递归的形式给出:如果两个 conses 各自car的 s 和cdrs 类似。(该规则甚至无法针对圆形对象进行评估;它不会终止)。

上述工作是因为实现超出了规范中的要求;他们提供的扩展符合(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 的相似性规则,它不需要复制为包含在第二和第三位置两次出现的相同对象的列表。

如果上述工作按预期工作,那是由于“对相似性规则的扩展”。

因此,“为什么不能 CLISP ...”问题的答案是,尽管 CLISP 确实提供了相似性的扩展,它保留了文字形式的图形结构,但它并没有在整个编译文件中这样做,仅在单个该文件中的顶级项目。(它用于*print-circle*发出单个项目。)错误是 CLISP 不符合用户可以想象的最佳行为,或者至少不符合其他实现表现出的更好行为。

于 2015-12-08T20:34:08.113 回答