【问题标题】:Why does lisp use gensym and other languages don't?为什么 lisp 使用 gensym 而其他语言不使用?
【发布时间】:2015-05-07 17:39:53
【问题描述】:

如果我错了,请纠正我,但在 Java、C、C++、Python、Javascript 或我用过的任何其他语言中没有像 gensym 这样的东西,而且我似乎从来不需要它。为什么在 Lisp 中需要它而不在其他语言中?为了澄清起见,我正在学习 Common Lisp。

【问题讨论】:

  • Gensym 与您在 Java 中使用 new Symbol("foo"); 所做的并没有什么不同。 interning 符号实际上更有趣(但一点也不独特)。 Intern 就像符号的工厂方法,如果您之前创建了类似的,您可以在其中得到相同的方法; gensym 就像一个构造函数,你总能得到新的东西。
  • 其实有坏的。参见 GCC 的 __COUNTER__,在 Linux Kernel 的 __UNIQUE_ID 等情况下使用 example。然而,即使是更理智的gensym 通常也不太可能是一个好主意,因为在语言规则正式变得不合理之前,依赖与符号解析过程交互的方法对语言的底层语义干扰太大。必要时,卫生也将难以维护。还有像Kernel's keyed static variables这样的替代品。

标签: lisp common-lisp


【解决方案1】:

Common Lisp 有一个强大的宏系统。您可以创建完全按照您希望它们的行为方式运行的新语法模式。它甚至用它自己的语言表达,使该语言中的所有内容都可用于将代码从您想要编写的内容转换为 CL 实际理解的内容。所有具有强大宏系统的语言都提供gensym 或在其宏实现中隐式提供。

在 Common Lisp 中,当您想要编写符号不应匹配结果中任何其他位置的元素的代码时,您可以使用 gensym。没有它,就无法保证用户使用宏实现者也使用的符号,并且它们开始干扰,结果与预期行为不同。它确保同一宏的嵌套扩展不会干扰以前的扩展。使用 Common Lisp 宏系统,可以创建类似于 Scheme syntax-rules 和 syntax-case 的限制性更强的宏系统。

Scheme 中有几个宏系统。一种模式匹配,其中新引入的符号会自动运行,就好像它们是用gensym 制作的一样。默认情况下,syntax-case 还会创建新符号,就好像它们是用 gensym 制作的一样,并且还有一种降低卫生的方法。你可以用syntax-case 制作CL defmacro,但由于Scheme 没有gensym,你不能用它制作卫生的宏。

Java、C、C++、Python、Javascript 都是Algol dialects,除了简单的基于模板的宏之外,它们都没有。因此他们没有gensym,因为他们不需要它。因为在这些语言中引入新语法的唯一方法是希望它的下一个版本能够提供它。

我想到了两种具有强大宏的 Algol 方言。 Nemerle 和 Perl6。它们都具有卫生方法,这意味着引入的变量的行为就像它们是用 gensym 制作的一样。

在 CL、Scheme、Nemerle、Perl6 中,您无需等待语言特性。你可以自己做!如果 Java 和 PHP 中的新闻不可用,它们中的任何一个都可以使用宏轻松实现。

【讨论】:

  • 在 Javascript 中,您使用“var”来定义一个新变量并将其限制在最严格的词法范围内。使用 Gensym 是因为 CL 中没有“var”之类的东西吗?
  • @michaelAdam Javascript 没有强大的宏。例如。实现cond,这样就可以了cond (expression) {...}(expression2) {...} else {...}
  • 我想我现在明白了。我不明白宏和函数之间的真正区别以及为什么宏中的变量不能像函数中那样仔细地限定范围。在函数中,您可以随意命名词法变量,因为您只是在处理对值的引用,更改它的名称并不会改变它的值。宏的参数不是包含值的变量,而是包含代码。该代码中的表达式名称不是任意的,它们来自宏的环境并确定它的生产。
  • @michaelAdam 是的,而且由于 CL 不会自动对您介绍的每个符号进行卫生处理,例如。制作一个临时变量,可能会影响或干扰宏被使用的地方的词法范围。 gensym 保证您可以使用一个唯一的符号来确保宏的卫生。
  • @RainerJoswig 我添加了一些链接。通常我的拼写很糟糕,但我对这个很满意:)
【解决方案2】:

无法说明哪些语言具有 GENSYM 的等价物。许多语言没有一流的符号数据类型(具有内部和非内部符号),并且许多语言没有提供类似的代码生成(宏,...)工具。

一个interned symbol注册在一个package中。 uninterned 不是。如果阅读器(阅读器是 Lisp 子系统,它将文本 s 表达式作为输入并返回数据)在同一个包中看到两个同名的内部符号,它假定它是相同的符号:

CL-USER 35 > (eq 'cl:list 'cl:list)
T

如果读者看到了一个未执行的符号,它会创建一个新符号:

CL-USER 36 > (eq '#:list '#:list)
NIL

Uninterned 符号在名称前写有#:。

GENSYM 在 Lisp 中用于创建 编号 非驻留符号,因为它有时在代码生成和调试代码中很有用。请注意,这些符号始终是新的,而不是 eq 对其他任何东西。但符号名称可能与另一个符号的名称相同。该数字为人类读者提供了有关身份的线索。

使用MAKE-SYMBOL的示例

make-symbol 使用字符串参数作为名称创建一个新的 uninterned 符号。

让我们看看这个函数生成一些代码:

CL-USER 31 > (defun make-tagbody (exp test)
               (let ((start-symbol (make-symbol "start"))
                     (exit-symbol  (make-symbol "exit")))
                 `(tagbody ,start-symbol
                           ,exp
                           (if ,test
                               (go ,start-symbol)
                             (go ,exit-symbol))
                           ,exit-symbol)))
MAKE-TAGBODY

CL-USER 32 > (pprint (make-tagbody '(incf i) '(< i 10)))

(TAGBODY
 #:|start| (INCF I)
         (IF (< I 10) (GO #:|start|) (GO #:|exit|))
 #:|exit|)

上面生成的代码使用了非内部符号。 #:|start| 实际上是同一个符号。如果我们有*print-circle* 到T,我们会看到这一点,因为打印机会清楚地标记相同的对象。但是在这里我们没有得到这个附加信息。现在,如果您嵌套此代码,那么您会看到不止一个 start 和一个 exit 符号,每个符号都在两个地方使用。

使用GENSYM的示例

现在让我们使用gensym。 Gensym 还创建了一个不受限制的符号。可选地,此符号由字符串命名。添加了一个数字(参见变量CL:*GENSYM-COUNTER*)。

CL-USER 33 > (defun make-tagbody (exp test)
               (let ((start-symbol (gensym "start"))
                     (exit-symbol  (gensym "exit")))
                 `(tagbody ,start-symbol
                           ,exp
                           (if ,test
                               (go ,start-symbol)
                             (go ,exit-symbol))
                           ,exit-symbol)))
MAKE-TAGBODY

CL-USER 34 > (pprint (make-tagbody '(incf i) '(< i 10)))

(TAGBODY
 #:|start213051| (INCF I)
         (IF (< I 10) (GO #:|start213051|) (GO #:|exit213052|))
 #:|exit213052|)

现在这个数字表明这两个未留存的#:|start213051| 符号实际上是相同的。当代码被嵌套时,新版本的开始符号将有一个不同的数字:

CL-USER 7 > (pprint (make-tagbody `(progn
                                     (incf i)
                                     (setf j 0)
                                     ,(make-tagbody '(incf ij) '(< j 10)))
                                  '(< i 10)))

(TAGBODY
 #:|start2756| (PROGN
                 (INCF I)
                 (SETF J 0)
                 (TAGBODY
                  #:|start2754| (INCF IJ)
                          (IF (< J 10)
                              (GO #:|start2754|)
                            (GO #:|exit2755|))
                  #:|exit2755|))
         (IF (< I 10) (GO #:|start2756|) (GO #:|exit2757|))
 #:|exit2757|)

因此它有助于理解生成的代码,而无需打开*print-circle*,这将标记相同的对象:

CL-USER 8 > (let ((*print-circle* t))
              (pprint (make-tagbody `(progn
                                       (incf i)
                                       (setf j 0)
                                       ,(make-tagbody '(incf ij) '(< j 10)))
                                    '(< i 10))))

(TAGBODY
 #3=#:|start1303| (PROGN
                    (INCF I)
                    (SETF J 0)
                    (TAGBODY
                     #1=#:|start1301| (INCF IJ)
                             (IF (< J 10) (GO #1#) (GO #2=#:|exit1302|))
                     #2#))
         (IF (< I 10) (GO #3#) (GO #4=#:|exit1304|))
 #4#)

Lisp reader(读取文本表示的 s-expressions 的子系统)可以阅读以上内容,但对于人类阅读者来说则略逊一筹。

【讨论】:

    【解决方案3】:

    我相信symbols(在 Lisp 意义上)在homoiconic 语言(其中语言的语法可以表示为该语言的数据)中最有用。

    Java、C、C++、Python、Javascript 不是同音字。

    一旦你有了符号,你就需要某种方式来动态地创建它们。 gensym 是可能的,但你也可以intern 他们。

    顺便说一句,MELT 是一种类似 lisp 的方言,它不会使用 gensym 或通过插入字符串而是使用 clone_symbol 创建符号。 (实际上 MELT 符号是预定义的 CLASS_SYMBOL, ... 的实例。

    【讨论】:

      【解决方案4】:

      gensym 在大多数 Prolog 解释器中都可用作谓词。您可以在同名库中找到它。

      【讨论】:

        猜你喜欢
        • 2012-08-22
        • 1970-01-01
        • 2015-07-19
        • 1970-01-01
        • 2010-10-07
        • 2012-01-05
        • 1970-01-01
        • 2018-04-29
        • 1970-01-01
        相关资源
        最近更新 更多