Broken equality for sets?

157 views
Skip to first unread message

Justin Kramer

unread,
Nov 9, 2012, 12:46:12 AM11/9/12
to cloju...@googlegroups.com
Steve Losh uncovered what looks like a bug:


This affects Clojure 1.4 and 1.5 master. The offending line seems to be:


If I understand correctly, equals should not be checking hashCode. That check was removed from APersistentMap in the following commit:


I removed the hashCode check in a local branch, which appears to fix the issue (and all tests pass). I can submit a patch if this is truly a bug.

Justin

Herwig Hochleitner

unread,
Nov 9, 2012, 2:20:10 AM11/9/12
to cloju...@googlegroups.com
Sure looks like a bug to me. I think opening a ticket is a safe bet here ;-)


2012/11/9 Justin Kramer <jkkr...@gmail.com>

Justin

--
You received this message because you are subscribed to the Google Groups "Clojure Dev" group.
To view this discussion on the web visit https://groups.google.com/d/msg/clojure-dev/-/lQXUC9hPvuMJ.
To post to this group, send email to cloju...@googlegroups.com.
To unsubscribe from this group, send email to clojure-dev...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/clojure-dev?hl=en.

Philip Potter

unread,
Nov 9, 2012, 2:41:18 AM11/9/12
to cloju...@googlegroups.com
That looks like a bug to me.

The = function in clojure delegates to Util.equiv(), which delegates
to the .equiv() method if present. The problem with sets is that
equiv() simply delegates to equals() when it should have a distinct
implementation (see vector for an example)

Clojure has two different equality relations: equals() & hashCode(),
which consider (Long. -1) and (Integer.-1) to be distinct, and equiv()
& hashEq(), which consider (Integer. -1) and (Long. -1) to be equal.
Clojure's persistent collections also implement these two different
equalities, so that [(Long. -1)] is equiv() but not equals() to
[(Integer. -1)].

user=> (def v [(Long. -1)])
#'user/v
user=> (def v* [(Integer. -1)])
#'user/v*
user=> (= v v*)
true
user=> (.equals v v*)
false

The same behaviour should occur for sets, but it doesn't, because
equiv() delegates to equals():

user=> (def s #{(Long. -1)})
#'user/s
user=> (def s* #{(Integer. -1)})
#'user/s*
user=> (= s s*)
false ;;; should be true here, this is the bug
user=> (.equals s s*)
false ;;; this is correct

so definitely file a bug. I hope this helps you file a patch, too.

Phil

Justin Kramer

unread,
Nov 9, 2012, 10:49:11 AM11/9/12
to cloju...@googlegroups.com
Ticket created:


On Friday, November 9, 2012 2:41:20 AM UTC-5, Philip Potter wrote:
The same behaviour should occur for sets, but it doesn't, because
equiv() delegates to equals():

user=> (def s #{(Long. -1)})
#'user/s
user=> (def s* #{(Integer. -1)})
#'user/s*
user=> (= s s*)
false    ;;; should be true here, this is the bug
user=> (.equals s s*)
false    ;;; this is correct

so definitely file a bug. I hope this helps you file a patch, too.


With a patch that makes APersistentSet consistent with APersistentMap, both = and .equals return true. This is also the case for maps (with and without the patch):

user=> (def m {(Long. -1) :foo})
#'user/m
user=> (def m* {(Integer. -1) :foo})
#'user/m*
user=> (= m m*)
true
user=> (.equals m m*)
true

I'm not sure if this should be considered a separate issue from the set quality one.

Justin

Christophe Grand

unread,
Nov 9, 2012, 12:53:41 PM11/9/12
to clojure-dev
The behaviour is more subtle:

Currently maps are:
 .equals when their keys are .equiv and their values .equals.
 .equiv when their keys are .equiv and their values .equiv.

So sets delegating .equals to .equiv would be coherent with the current behaviour of maps.

Christophe



Justin

--
You received this message because you are subscribed to the Google Groups "Clojure Dev" group.
To view this discussion on the web visit https://groups.google.com/d/msg/clojure-dev/-/tQWIeFrpL2cJ.

To post to this group, send email to cloju...@googlegroups.com.
To unsubscribe from this group, send email to clojure-dev...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/clojure-dev?hl=en.



--
Professional: http://cgrand.net/ (fr)
On Clojure: http://clj-me.cgrand.net/ (en)

Andy Fingerhut

unread,
Nov 9, 2012, 7:50:06 PM11/9/12
to cloju...@googlegroups.com
It seems that there have been around half a dozen tickets on issues with = and hasheq in the last year or so.

Is it a good time to have some more extensive unit tests in this area that try to hit all of the concrete data structures several times each in this area?

Andy

Philip Potter

unread,
Nov 12, 2012, 3:47:04 PM11/12/12
to cloju...@googlegroups.com
Hmm. This behaviour breaks .equals() symmetry & transitivity, and
.equiv() transitivity, for operations crossing j.u.Map and
IPersistentMap:

user=> (def a (doto (java.util.HashMap.) (.put (Long. -1) -1)))
#'user/a
user=> (def b {(Long. -1) -1})
#'user/b
user=> (def c {(Integer. -1) -1})
#'user/c
user=> (= a b)
true
user=> (= b c)
true
user=> (= a c)
false

user=> (.equals a c)
true
user=> (.equals c a)
false
user=> (.equals c b)
true
user=> (.equals b a)
true

...though that said, I can see fairly solid-looking design reasons why
this behaviour is pragmatically useful. Should I file a bug (or two)?

Christophe Grand

unread,
Nov 12, 2012, 4:34:58 PM11/12/12
to clojure-dev
On Assembla, the design page on numerics reads:

hash maps and sets use = for keys
  • this will make maps and sets unify 42 and 42N, since =
  • will use stricter .equals when calling through java.util interfaces
    • not there yet
    • this will require renaming Associative.containsKey, since semantics will now differ
So it looks like no one volunteered to fully execute the plan — or this page is not the final word on numerics.

Christophe

Philip Potter

unread,
Nov 13, 2012, 2:43:51 AM11/13/12
to cloju...@googlegroups.com
The same is still on confluence:

http://dev.clojure.org/display/doc/Enhanced+Primitive+Support

If matching java.util interfaces closely is the desired design, does
.equals() count as a java.util interface method? Should it check the
keys are .equals rather than .equiv?

Rich Hickey

unread,
Nov 13, 2012, 4:46:38 PM11/13/12
to cloju...@googlegroups.com
It's more subtle than that, as we still have yet to break out containsKey and have it conform to the contract of j.u.Map, at which point .equals will be .equals for key and value.

In any case, all thinking on this needs to consider java.util conformance as well.

Christophe Grand

unread,
Nov 13, 2012, 5:11:51 PM11/13/12
to clojure-dev
Thanks, good to know that the design notes on numerics I quoted are still current.
It follows that, for example, .contains on sets, seqs and vectors should be fixed to conform to j.u.Collection. Right?

Rich Hickey

unread,
Nov 20, 2012, 8:07:15 PM11/20/12
to cloju...@googlegroups.com
Yes. We might have to audit our use and see what needs to change to 'containsEquiv', if anything.

Christophe Grand

unread,
Nov 21, 2012, 9:51:27 AM11/21/12
to clojure-dev
Thianking about it, isn't the j.u.Map contract too strong in its wording?
TreeMaps are Maps but neither get, put or containsKey obey j.u.Map because the comparator defines its own equivalence relation. (btw TreeMaps for equality, use their equivalence relationon keys and equality on values, like APersistentMap does)
Ditto for contains (in TreeSet for example).

So is it important to follow java.util.Map to the letter? Where/when does it matter?

Christophe

On Clojure http://clj-me.cgrand.net/
Clojure Programming http://clojurebook.com
Training, Consulting & Contracting http://lambdanext.eu/

Christophe Grand

unread,
Nov 22, 2012, 5:42:06 PM11/22/12
to clojure-dev
Please forget what I just said: in Comparator's doc it's recommended that comparators used with sorted collections are consistent with equals (define teh same equivalence relation) to not violate the general contract of maps/sets.

It follows that even .get and .valAt for example should diverge.
Reply all
Reply to author
Forward
0 new messages