【问题标题】:Why can't CLISP call certain functions with uninterned names?为什么 CLISP 不能使用非内部名称调用某些函数?
【发布时间】:2013-10-10 05:59:44
【问题描述】:

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

【问题讨论】:

  • 印刷表示中的符号相同。然而,当你写 #:foo 之类的东西时,读者会创建一个新符号,因此,例如,输入你的 REPL,你会得到 (eq '#:foo '#:foo) ;=> nil。您是否尝试通过输入函数名称来调用该函数?
  • @JoshuaTaylor 正如我在帖子中所说,这些符号是相同的符号。宏只调用一次gensym,并将结果放在展开式中的多个位置。当然,读取宏扩展会导致创建两个不同的符号,但事实并非如此。
  • 你能生成一些直接的代码来重现这个问题吗?我可以编写一个宏来定义一些相互递归的函数,这些函数由非内部符号命名,并且在 CLISP 中运行良好。参见,例如,pastebin.com/7MNGkgta。它有两个函数的名字是不固定的,一个函数的名字很容易调用,而且它似乎工作正常。
  • @JoshuaTaylor 如果不将其简化到据我所知,它可能开始起作用的地步,我真的无法做到这一点。如果您想给我发电子邮件(nbtrap AT nbtrap DOT com),我可以将实际项目代码发送给您,您会看到,该代码在 SBCL 上运行良好,但在 CLISP 下运行良好。问题是生成解析器的宏过于复杂,无法在此处简化为示例,尽管它生成的代码相当简单(大量 case 表达式的集合)。
  • 哦,一些额外的谷歌搜索可能会出现这种情况:看看这个错误报告#180 uninterned symbols not shared between forms in FAS file。我发现它是从this mailing list discussion 链接的。这与您的实际代码有关吗?也可以看看#281 Lexical Binding in Toplevel PROGN (Clisp 2.35)

标签: lisp common-lisp sbcl clisp


【解决方案1】:

您没有显示给您错误的行之一,所以我只能猜测,但据我所知,唯一可能导致此问题的是您指的是符号的名称尝试调用它时而不是符号本身。

如果您指的是符号本身,那么您的 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 的行为取决于代码是直接解释还是先编译然后再解释。您可以通过在 CLISP 中直接 loading 一个 Lisp 文件或首先调用 compile-file 然后 loading 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 的案例。

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

【讨论】:

  • 好问题。我不使用labels 或内联函数(这是我最初所做的)的原因是在这种情况下生成的单个函数太大(> 30,000 行)以至于编译器内存不足并且不能t 编译它。
  • 以我对原始问题的编辑为例。
  • 感谢您的建议,但defuns 的顺序肯定是正确的。调用发生在定义之后。
  • 您给出的示例与我正在处理的示例之间的一个区别是,我不直接调用具有 uninterned 名称的函数。相反,我调用顶层函数,它依次调用一个或多个具有非内部名称的函数。使用您的示例,我从 REPL 测试了一个更类似的案例,它仍然有效。嗯……
  • @Rörd 看编译是一个很好的轨道。我发现了一些错误报告,并在 this comment 中提到了主要问题,但也许(因为你已经得到了接受的答案),你可以将链接添加到你的答案中。
【解决方案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 的 uninterned 符号。每次遇到这种语法时,都会创建一个不同的非内部符号。 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

【讨论】:

  • 在我最初的问题中,我试图明确表示我理解这一点并且这不是问题。
  • 我确实看到“#:G75735 的两个实例绝对是同一个符号——不是两个同名的不同符号。”但我不清楚你是如何尝试调用函数的。很抱歉有任何混淆。不过,我发布的带有非实习名称的函数的代码在 CLISP 中有效。你能想出一个我们也可以在 CLISP 中尝试的最小工作示例吗?
  • 是的,也许我不够清楚。我在原帖中给出的代码来自宏扩展——这不是读者见过的。
【解决方案3】:

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

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

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

现在我可以得到(setf x (test)) 的名字并称之为(funcall x 2)

【讨论】:

  • 原来这是 CLISP 中的longstanding bug。看起来如果你有,而不是',name 作为最后一个形式,例如(,name 45),并将(test) 放在一个文件中,然后编译并加载该文件,你可能已重现错误。
  • @JoshuaTaylor 实际上它有效。如果我用(,name 45) 替换',name 并将其保存在“test.lisp”中,我可以调用(load (compile-file "test.lisp"))(test) 给出2025。我在Mac OS X 10.5.8 下使用GNU CLISP 2.49。
  • 显然,我也没有测试过它。也许如果不是直接调用,而是定义一些其他调用它的函数?并尝试在编译和加载后调用它?错误报告使它看起来不应该进行太多修改来破坏事情。 :)
  • 是的,这是一个显示错误 pastebin.com/ux40R8uE 的示例。只需将其放在一个文件中,编译并加载,然后调用(frob)
  • 对不起,我写错了。不要调用(frob),调用(call-frob)应该调用伪frob那个有错误。我写得太快了。不过,该代码重现了该错误。我只是在我的描述中有一个错误。 :) 这是一个带有更好说明的粘贴:pastebin.com/pBTUJr5B
【解决方案4】:

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

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

【讨论】:

  • 我不想“按名字”称呼他们。我正在按名称调用不同的函数,该函数又会调用具有未使用名称的函数。
【解决方案5】:

令人惊讶的是,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),uninterned符号的规则是相似度是基于名称的。如果我们使用包含非驻留符号的文字来编译代码,那么当我们加载编译后的文件时,我们会得到一个相似的符号,而不是(必然)相同的对象:具有相同 name的符号>.

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

但情况会变得更糟:同样没有要求以相同的形式出现的两个相同的非驻留符号将被外部化,以保持它们的相对身份:该对象的加载版本将在原始文件所在的所有位置具有相同的符号对象。事实上,相似性的定义并未包含保留循环结构和子结构共享的规定。如果我们有像'#1=(a b . #1#) 这样的文字,作为编译文件中的文字,似乎没有要求将其复制为具有与原始图形结构相同的圆形对象(图形同构)。 conses 的相似性规则以朴素递归的形式给出:如果两个 conses 各自的 cars 和 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 和 symbol 的相似性规则,它不需要复制为包含在第二和第三位置两次出现的相同对象的列表。

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

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

【讨论】:

    猜你喜欢
    • 2016-03-13
    • 2015-05-25
    • 2017-12-04
    • 1970-01-01
    • 2021-09-14
    • 1970-01-01
    • 1970-01-01
    • 2017-05-29
    相关资源
    最近更新 更多