【问题标题】:What does eq? predicate do in mit scheme interpreter?eq 是什么? MIT 方案解释​​器中的谓词?
【发布时间】:2017-02-19 14:44:38
【问题描述】:

我知道 (eq? obj1 obj2) 仅在 obj1 和 obj2 都引用内存中的同一个对象时才返回 #t

1) (eq? 2.6 2.6) 返回#f,因为它们是浮点数并且浮点数的表示在方案中是不同的。有人能解释一下它们在记忆中是如何表示的吗?

2) 为什么对和非空字符串经常返回#f,即使我们之前没有声明它们?示例:(eq? (cons 1 2) (cons 1 2)),同时将相同的数字与 eq? 进行比较,得出与 equal? 谓词相同的结果。

例如,(define x 3) (define y 3) (eq? x y) 返回#t

我也知道(eq? '() '()) 返回#t,因为内存中只有一个空列表表示,即指向 0 的指针。

【问题讨论】:

    标签: functional-programming scheme lisp


    【解决方案1】:

    要理解这一点,您确实需要了解 Lisps 通常如何表示对象。以下内容并非特定于 MIT 方案,甚至可能对它(或任何特定实现)都不正确,但旨在让您了解一般的考虑因素。除了下面概述的之外,还有其他可能的表示,但它们都有类似的权衡。

    对象表示

    首先,系统需要知道关于任何对象的两件事:

    • 它是什么东西——它的类型;
    • 它的值是什么——代表对象的某种位模式。

    当然,位模式仅在类型的上下文中才有意义。

    有很多类型,在许多 Lisp 方言中,您可以创建新类型,因此例如说“只能有这六种东西”是行不通的。但是有些类型比其他类型更重要(我要说“有些类型比其他类型更平等”):例如,您可能希望较小的整数非常有效。

    嗯,在大多数情况下,您最终会得到某种指向对象实际数据的指针。但是现在你可以做一个聪明的把戏:大多数(所有?)现代机器都是字节寻址的——内存地址以 8 位的粒度工作。但是,如果您只想指向较大的对象,则指针中有多余的位。因此,如果您在 64 位机器上并且只想指向 64 位边界上的事物,则永远不会使用指针的低 3 位,并且如果您只想指向 128 位边界,那么您有4 个备用位。

    所以你可以做一个狡猾的把戏:在一个 64 位字中你可以打包一个指针,在你不需要的指针底部的几个位中,一个可以编码少数最重要的类型。

    你可以做得比这更好:对于一些小对象,你可以将指针所在的对象本身打包。典型的例子是小整数——Lisps 历史上称为 fixnums 的东西。因此,用您的话来说,您现在有一个标签位模式,上面写着“这是一个固定编号”,而固定编号的实际位向右移动以为标签腾出空间。 (实际上,你通常有两个标签用于'even fixnum'和'odd fixnum',这让你可以做一个狡猾的技巧来让fixnums更大一点,我会让你去解决)。 Lisps 传统上也会对NIL / () 执行此操作,这可能传统上是一个特殊的“nil”标签,单词的其余部分为零。这对 Scheme 来说可能意义不大,我不确定。

    但是一般情况下你不能这样做:大多数对象都必须用指针存储,在最一般的情况下,单词中的标记位会说“它是别的东西”,指针会指向某个对象,既具有事物的类型,也具有事物的价值,并且会有几个词长。

    像小整数一样立即存储的对象通常称为“未装箱”,而一般基于指针的对象称为“装箱”。

    eq? 做了什么

    那么,eq? 做了什么?很简单:它告诉您这两个词是否是相同的位模式:如果标记和指针/立即值都是相同的位。这非常快(单条指令),但也非常原始。

    对于诸如 fixnums 之类的直接类型,它会告诉您两个对象是否具有相同的值。对于指针类型,它会告诉您两个对象是否相同:如果两个指针指向相同的地址(并且具有相同的标记位,尽管如果它们不指向会很奇怪)。它不会做的是告诉你两个指针类型是否表示相同的值,即使它们是不同的指针。

    实习:一招

    您可以为盒装类型做的一个技巧是实习它们:每次你要制作一个新的时,你看看你之前是否制作过一个等效的,并且,如果你有,你只需返回一个指向同一个对象的指针。这意味着eq? 将为任何两个等效的实习对象返回true,因为它们实际上是同一个对象。只有在对象数量相对较少、它们是不可变的或实际上不可变的、或者如果您有意想要这种实习行为时,实习对象才真正有意义。

    浮点数

    浮点数是 32 位或 64 位。因此,在 64 位系统上,您可以立即表示 32 位单浮点数:在一半的字中有一些标记位和一堆零,在另一半有浮点数的位。但是你永远不能对双浮点数这样做。即使对于单个浮动,您也需要一个特殊的“浮动”标签,而标签是一种极其稀缺的资源——可能只有 8 个。 (当然,大多数 Lisp 在活内存之前都是 32 位的,而 32 位的 Lisp 甚至不能立即存储单个浮点数。)

    所以,一般来说,单浮点和双浮点都是装箱的。实习浮点数没有意义,因为您可以生成大量的浮点数:每秒十亿个或其他东西,而且它们通常不一样。

    所以eq? 通常会在两个浮点数上返回 false,即使它们的数值相同。但是你不能真正依赖它:这样的东西应该做什么?

    (define (x f)
      (let ([g f])
        (eq? g f)))
    (x 1.0)
    

    它可能会返回#t,但我不确定它是否必须这样做。我相当确定 CL 中的等效项没有明确定义,例如:允许实现复制浮点数。

    最后,浮点计算通常是人们非常关心性能的地方,而装箱的浮点数对性能的影响很大。因此,具有适当类型声明的实现通常可以编译将浮点数视为即时、未装箱(和未标记)的对象以提高性能的代码。 eq? 对于数值相等的浮点数可能会返回 true。但答案是,如果你想进行数值比较,请使用=:这就是它的用途。

    小整数

    参见上面的讨论:eq? 通常适用于较小的整数,因为它们会立即存储。但它不适用于 all 整数:

    > (eq? 1 1)
    #t
    > (eq? (expt 2 20) (expt 2 20))
    #t
    > (eq? (expt 2 64) (expt 2 64))
    #f
    > (eq? (expt 2 62) (expt 2 62))
    #f
    > (eq? (expt 2 61) (expt 2 61))
    #t
    > (eq? (- (expt 2 62) 1) (- (expt 2 62) 1))
    #t
    

    所以对于这个实现(Racket),你可以在这里看到未装箱和装箱整数之间的转换。

    但据我所知,没有要求立即存储 any 整数。再说一遍:使用=,这就是它的用途。

    缺点

    Conses 总是被装箱并且只比较 eq? 如果它们是相同的 cons。特别是 cons 的工作是创建新的 conses,所以 (eq? (cons '() '()) (cons '() '()) 永远不会是真的。

    符号

    符号是规范的实习类型:(eq 'x 'x) 为真,因为它是符号的工作使其为真:当读取一个符号时,它是实习的,并且读取具有相同名称的其他符号将(除了在奇怪的情况)返回相同的对象。

    我对 Scheme 中的符号了解不多,但在 CL 中,您可以创建 uninterned 符号,尽管它们看起来相同,但实际上并非如此。这些对于 CL 中的宏中的事物名称很有用(Scheme 有一个更聪明的方法)。

    字符串

    这些可能会被实习,但可能只是其中的一部分,你不能依赖这个。因此,例如在 Racket 中,我认为您可以依赖 (eq? "foo" "foo") 为真,但也许不是,如果可以的话,您可能只能依赖它,因为这两个字符串都来自读者。同样,您应该使用正确的谓词。

    特殊类型

    #t() 之类的东西要么没有装箱,要么是独一无二的(这是它们生命中的全部意义),所以 eq? 对它们起作用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-15
      • 1970-01-01
      • 1970-01-01
      • 2021-08-01
      相关资源
      最近更新 更多