在Scheme中,eq?、eqv?、equal?和=有何不同?

108
我想知道在Scheme中这些操作之间的区别。我在Stack Overflow上看到了类似的问题,但是它们都是关于Lisp的,并且没有对这三种运算符进行比较。
我正在编写Scheme中不同类型的命令,并获得以下输出:
(eq? 5 5) -->#t
(eq? 2.5 2.5) -->#f
(equal? 2.5 2.5) --> #t
(= 2.5 2.5) --> #t

为什么会这样?

6
还有一个名为 eqv? 的函数,它的意思与 eq?equal? 不同。 - newacct
7个回答

190

我将逐步回答这个问题。让我们从=等价谓词开始。 =谓词用于检查两个数字是否相等。 如果您提供给它的是除数字以外的任何内容,它都会引发错误:

(= 2 3)     => #f
(= 2.5 2.5) => #t
(= '() '()) => error
< p > eq?谓词用于检查其两个参数是否在内存中表示相同的对象。例如:

(define x '(2 3))
(define y '(2 3))
(eq? x y)         => #f
(define y x)
(eq? x y)         => #t

需要注意的是,在内存中只有一个空列表'()(实际上,空列表不存在于内存中,但指向内存位置0的指针被视为空列表)。因此,比较空列表eq?将始终返回#t(因为它们代表相同的内存对象):

(define x '())
(define y '())
(eq? x y)      => #t

现在根据实现,eq?对于诸如数字、字符串等原始值可能会返回#t或者不返回。例如:

(eq? 2 2)     => depends upon the implementation
(eq? "a" "a") => depends upon the implementation

这就是eqv?谓词发挥作用的地方。 eqv?eq?谓词完全相同,但对于相同的原始值,它将始终返回#t。例如:
(eqv? 2 2)     => #t
(eqv? "a" "a") => depends upon the implementation

因此,eqv?eq?的超集,在大多数情况下,您应该使用eqv?而不是eq?
最后我们来到equal?谓词。 equal?谓词与eqv?谓词完全相同,除了它还可以用于测试两个列表,向量等是否具有满足eqv?谓词的对应元素。例如:
(define x '(2 3))
(define y '(2 3))
(equal? x y)      => #t
(eqv? x y)        => #f

一般而言:

  1. 当您想测试两个数字是否相等时,请使用=谓词。
  2. 当您想测试两个非数字值是否相等时,请使用eqv?谓词。
  3. 当您想测试两个列表、向量等是否相等时,请使用equal?谓词。
  4. 除非您确切知道自己在做什么,否则不要使用eq?谓词。

8
据我所知,(eqv? "a" "a") ==> unspecified。你将需要使用equal?或者(可能更优化的)string=? - Sylwester
3
根据该报告(eq? '(1) '(1))的结果是未指定的,因此你的(define x '(1 2))示例可能不起作用。 - Will Ness
4
非常准确和信息丰富。特别是最后的指南。 - Germán Diago
2
但是eq?似乎是针对符号定义的,这一点需要注意!如果符号看起来相同,eq?返回#t。例如(eq?'foo'foo)->#t(eq?'foo'bar)->false。我在这里读到了这个链接链接 - Nedko

15

在RnRS规范中,有两整页的内容涉及eq?,eqv?,equal?和=。 这是R7RS规范草案,请查看!

说明:

  • =比较数字,2.5和2.5在数值上相等。
  • equal?对于数字降为=,2.5和2.5在数值上相等。
  • eq?比较“指针”。在您的Scheme实现中,数字5作为“立即数”(可能)实现,因此5和5是相同的。数字2.5可能需要在您的Scheme实现中分配'浮点记录'来存储,这两个指针不相同。

1
截至2018年2月4日,Draft R7RS规范的链接已失效。 - anon
2
更新为实时链接。 - GoZoner
关于数字的equal?/eqv?的评论在某种程度上是不正确的。equal?/eqv?会在两个操作数具有相同的精确度时缩减为=。所以(= 1 1.0)是真的,而(eqv? 1 1.0)是假的。 - undefined

12

eq?是当地址/对象相同时为#t。一般来说,我们可以期望对于相同的符号、布尔值和对象,以及不同类型、不同值或不同结构的值,返回#f。Scheme/Lisp实现有将类型嵌入指针并在相同空间内嵌入值的传统。因此,某些指针实际上不是地址而是值,例如字符R或Fixnum 10。由于“地址”是嵌入的类型+值,因此这些将是eq?。一些实现还会重用不可变常量。当解释时,(eq? '(1 2 3) '(1 2 3))可能为#f,但编译后可能为#t,因为它可能获得相同的地址(类似于Java中的常量字符串池)。由于这个原因,许多涉及eq?的表达式是未指定的,因此它是否评估为#t#f取决于实现。

eqv?对于与eq?相同的内容为#t。如果它是数字或字符且其值相同,则也为#t,即使数据太大而无法适应指针。因此,对于这些内容,eqv?会额外检查类型是否受支持,两者是否是相同类型以及其目标对象是否具有相同的数据值。

equal?对于与eqv?相同的内容为#t,如果它是一种复合类型(例如pair、vector、string和bytevector),则会递归执行equal?操作。在实践中,如果两个对象看起来相同,则返回#t。在R6RS之前,在循环结构上使用equal?是不安全的。

=类似于eqv?,但它只适用于数字类型。它可能更有效率。

string=?类似于equal?,但它只适用于字符串。它可能更有效率。


6

equal? 递归比较两个对象(任意类型)是否相等。

  • 请注意,对于一个大型数据结构来说,这可能会非常耗费时间,因为必须遍历整个列表、字符串、向量等。

  • 如果对象只包含单个元素(例如:数字、字符等),则与eqv?相同。


eqv?测试两个对象,以确定两者是否“通常被视为相同的对象”。

  • eqv?eq?是非常相似的操作,它们之间的区别在某种程度上将有些具体实现。

eq?eqv?相同,但可以识别更细微的差别,并且可以实现更高效。

  • 根据规范,这可以实现为快速有效的指针比较,而不是eqv?的更复杂运算。


=用于比较数值相等性。

  • 请注意,可以提供多于两个数字,例如:(= 1 1.0 1/1 2/2)

我原以为eq?是实际指针相等性(而不是eqv?)。它是“最好的或最具辨别力的”。例如(eqv? 2 2)保证是#t,但(eq? 2 2)是“未指定的”。也就是说,这取决于实现是否为每个新读取的数字创建实际的新内存对象,或者如果可以的话重用先前创建的对象。 - Will Ness
@WillNess - 很好的发现,谢谢。eq?eqv?之间的差异比其他操作更微妙。 - Justin Ethier

5
您没有提到所使用的Scheme实现方式,但在Racket中,eq?仅在参数指向相同对象时返回true。您的第二个示例返回#f是因为系统为每个参数创建了一个新的浮点数;它们不是同一个对象。 equal?=用于检查值的等价性,但=只适用于数字。
如果您使用的是Racket,请在此处查看更多信息。否则,请查阅您的Scheme实现文档。

3
最好的方法是阅读规范文档:http://www.r6rs.org/final/html/r6rs/r6rs-Z-H-14.html#node_sec_11.5 - Dirk

3
eq?视为指针相等。报告的作者希望它尽可能通用,因此他们没有明确说明这一点,因为它取决于实现,并且说出来会支持基于指针的实现。但是,他们确实说了:
它通常比eqv?更容易实现,例如,作为简单的指针比较
下面是我的意思。(eqv? 2 2)保证返回#t,但(eq? 2 2)是未指定的。现在想象一个基于指针的实现。在其中,eq?只是指针比较。由于(eq? 2 2)是未指定的,因此这意味着该实现可以自由地创建每个从源代码中读取的新数字的新内存对象表示。而eqv?必须实际检查其参数。
OTOH (eq 'a 'a)#t。这意味着这样的实现必须识别具有重复名称的符号,并在内存中为所有符号使用相同的一个表示对象。
假设一个实现不是基于指针的。只要它遵循报告,就没有关系。作者只是不想被视为向实现者规定具体实现细节,因此他们选择了仔细的措辞。
这是我猜测的。所以粗略地说,eq?是指针相等性,eqv?是(原子)值感知的,equal?也是结构感知的(递归检查其参数,以便最终(equal? '(a) '(a))需要是#t),=用于数字,string=?用于字符串,详细信息请参见报告。

1
除了之前的答案,我想再添加一些评论。
所有这些谓词都想要在不同的上下文中定义对象的抽象功能,但是目的相同。
EQ?依赖于具体实现,不能回答“两个对象是否相同”的问题,只能在有限的情况下使用。从实现的角度来看,这个谓词只是比较两个数字(对象的指针),它并不关心对象的内容。例如,如果你的实现不是唯一地保存字符串,而是为每个字符串分配不同的内存,那么(eq? "a" "a")将返回false。
EQV?——它查看对象的内部,但使用也受到限制。如果(eqv? (lambda(x) x) (lambda(x) x))会返回true取决于具体实现的定义方式。现在我们知道某些快速方法可以比较某些函数的功能,但eqv?提供了大数、字符串等方面的连贯性答案。
实际上,其中一些谓词试图使用对象的抽象定义(数学上),而其他谓词则使用对象的表示(在真实机器上的实现方式)。身份的数学定义来自莱布尼兹,它说:
X = Y  iff  for any P, P(X) = P(Y)
X, Y being objects and
P being any property associated with object X and Y.

理想情况下,能够在计算机上实现这个定义,但由于不可判定性和/或速度等原因,它并没有被字面上地实现。这就是为什么有很多运算符尝试聚焦于此定义周围的不同视角。
试着想象一下对于连续体而言一个抽象身份的定义。即使你可以提供一组函数的定义(sigma-recursive class of functions),语言也不会强制任何谓词为真或为假。这将会使语言的定义变得更加复杂,甚至更加难以实现。
其他谓词的上下文分析则要容易得多。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接