defrecord equality

169 views
Skip to first unread message

Chouser

unread,
May 1, 2012, 3:07:58 PM5/1/12
to cloju...@googlegroups.com
In Clojure, instances of defrecord are not = to regular maps with the
same content, nor with other defrecord instances of other types. This
is clearly a design choice and has been discussed before, but I
haven't found anywhere a justification of this choice.

(ClojureScript doesn't behave exactly the same way, currently, but I
assume these are just bugs.)

Here are the bits of answers I have found:
The defrecord doc string mentions value-and-type equality, but doesn't
justify it. (note http://clojure.org/datatypes says only value-based
equality)
Rich describes including type in = as "most useful" here:
http://groups.google.com/group/clojure/msg/5a143969fe80bcf9
There is also discussion of why user-defined equality is undesirable,
with which I have no argument.

Related facts:
- lists and vectors have many differences (implementing IFn vs. not,
conj vs seq ordering) and yet they can be equal.
- most clojure collections fall into one of three equality
"partitions", but each defrecord type effectively defines it's own
partition
- defrecord equality depends on the concrete type of the record
itself, a rare dependence on concrete type in clojure

So why is it best for = to take into account the concrete types of
records, and also to ignore the differences in types between vectors
and lists?

--Chouser

Mark Engelberg

unread,
May 1, 2012, 3:18:28 PM5/1/12
to cloju...@googlegroups.com
I have a memory of a thread in which Rich said that in his mind, if you were going to the effort to build something with defrecord as opposed to just using a map, then that implies that you actually care about the type and consider it distinct from other records/maps with the same field, and so that should be part of the equality test.  If you don't care about type, use a map.

Kevin Downey

unread,
May 1, 2012, 3:33:07 PM5/1/12
to Clojure Dev
I wonder if that was before or after the deftype/defrecord split,
because I could certainly see the case for defrecord having map
equality since it otherwise behaves like a map

László Török

unread,
May 1, 2012, 3:33:11 PM5/1/12
to cloju...@googlegroups.com
In my interpretation, a clojure record is a (map,type) tuple, so strictly speaking 

a (map1,type1) can't really equal to a (map2) even if map1 = map2 as they aren't from the same domain (in math sense)

one can of course define a a different equivalence relation where every (map1,type1) eq map2 if map1 = map2

but that will be a different operator.

2012/5/1 Mark Engelberg <mark.en...@gmail.com>
I have a memory of a thread in which Rich said that in his mind, if you were going to the effort to build something with defrecord as opposed to just using a map, then that implies that you actually care about the type and consider it distinct from other records/maps with the same field, and so that should be part of the equality test.  If you don't care about type, use a map.


--
You received this message because you are subscribed to the Google Groups "Clojure Dev" group.
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.



--
László Török

Stuart Halloway

unread,
May 1, 2012, 3:50:28 PM5/1/12
to cloju...@googlegroups.com
I have a memory of a thread in which Rich said that in his mind, if you were going to the effort to build something with defrecord as opposed to just using a map, then that implies that you actually care about the type and consider it distinct from other records/maps with the same field, and so that should be part of the equality test.  If you don't care about type, use a map.

It is also useful to consider what records replaced: the use of explicit type tags in maps. These tags were part of data, and contributed to equality semantics.

Stuart Halloway
Clojure/core
http://clojure.com

Paul Stadig

unread,
May 1, 2012, 4:06:01 PM5/1/12
to cloju...@googlegroups.com
So I'm not sure if this situation is really that useful (it is mostly pathological/contrived), but you can assoc extra keys into a defrecord instance and it maintains its type:

user=> (defrecord Foo [bar baz])
user.Foo
user=> (def x (Foo. 1 2))
#'user/x
user=> x
#user.Foo{:bar 1, :baz 2}
user=> (assoc x :quux 3)
#user.Foo{:bar 1, :baz 2, :quux 3}

If you dissoc one of the basis keys from a defrecord it "downgrades" to a map:

user=> (dissoc x :bar)
{:baz 2}

If you then assoc back in the basis key you now have a map that is structurally equivalent to the original defrecord type, and compares equal with regular maps, but not a defrecord:

user=> (assoc (dissoc x :bar) :bar 1)
{:bar 1, :baz 2}
user=> (= (assoc (dissoc x :bar) :bar 1) {:bar 1 :baz 2})
true
user=> (= (assoc (dissoc x :bar) :bar 1) (Foo. 1 2))
false
user=> (= x (Foo. 1 2))
true

Not sure which side argument this example contributes to. I guess you could see dissoc'ing one of the basis keys as also implicitly dissoc'ing the type. Either way it shows some of the subtlety of defrecords being maps, but not really being maps.


Paul

Laurent PETIT

unread,
May 1, 2012, 4:06:12 PM5/1/12
to cloju...@googlegroups.com
I guess reasoning about records cannot be dissociated from the concept of protocols, and the focus should be placed on protocols, records being just one kind of actual type extending the protocol.

So ... should we consider 2 instances of 2 differerent records, with the same map-like data, to be equal by considering they behave the same with protocols ? (if they are equal, could they be interchangeable?). If so, then it's a good reason for the type to be included in the equality comparison (and I tend to think so).

Said differently: records are not implementations of abstract data types. Types are. ( and that's why you'll have ArrayMaps and HashMaps with same values being considered equal, I guess). And since they're not, they should not be equal just considering value semantics minus their "record name".

But maybe I'm wrong :)

2012/5/1 Chouser <cho...@n01se.net>

--Chouser

Chouser

unread,
May 1, 2012, 4:56:22 PM5/1/12
to cloju...@googlegroups.com
On Tue, May 1, 2012 at 3:50 PM, Stuart Halloway
<stuart....@gmail.com> wrote:
>
> It is also useful to consider what records replaced: the use of explicit
> type tags in maps. These tags were part of data, and contributed to equality
> semantics.

The 'type' function uses :type from the object's metadata which of
course does not contribute to equality.

Isn't this what defrecord replaced?

--Chouser

Mark Engelberg

unread,
May 1, 2012, 5:29:47 PM5/1/12
to cloju...@googlegroups.com
On Tue, May 1, 2012 at 1:56 PM, Chouser <cho...@gmail.com> wrote:
The 'type' function uses :type from the object's metadata which of
course does not contribute to equality.

Isn't this what defrecord replaced?

Not really.  If you look at clojure.org/multimethods, you'll see the following example:

(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:wd r) (:ht r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)

Notice how the "type" is embedded in the map, in this case, associated with the :Shape keyword.  Back in the multimethod days, lots of examples recommended that a nice generic keyword to contain a "type" for dispatching purposes was :tag, so that's what I still use in all my programs.

This is really what defrecord aims to replace, in my opinion.

Meikel Brandmeyer

unread,
May 1, 2012, 6:56:05 PM5/1/12
to cloju...@googlegroups.com
Hi,

Am 01.05.2012 um 23:29 schrieb Mark Engelberg:

> Notice how the "type" is embedded in the map, in this case, associated with the :Shape keyword. Back in the multimethod days, lots of examples recommended that a nice generic keyword to contain a "type" for dispatching purposes was :tag, so that's what I still use in all my programs.
>
> This is really what defrecord aims to replace, in my opinion.

That's just one way how to embed a type. Another one was the one Chouser described. There is a even a core function (type) which was added to support this. So historically we had both ways of embedding type. One did influence equality, one didn't.

Another proof that history proves nothing?

Kind regards,
Meikel

Mark Engelberg

unread,
May 1, 2012, 7:01:29 PM5/1/12
to cloju...@googlegroups.com
On Tue, May 1, 2012 at 3:56 PM, Meikel Brandmeyer <m...@kotka.de> wrote:
That's just one way how to embed a type. Another one was the one Chouser described. There is a even a core function (type) which was added to support this. So historically we had both ways of embedding type. One did influence equality, one didn't.


My hazy memory is that we were instructed that the metadata type tag was something reserved for use by Clojure itself for its own data structures, and that it was a bad idea to mess with it directly.

Meikel Brandmeyer

unread,
May 2, 2012, 2:07:20 AM5/2/12
to cloju...@googlegroups.com
Hi,


Am Mittwoch, 2. Mai 2012 01:01:29 UTC+2 schrieb puzzler:

My hazy memory is that we were instructed that the metadata type tag was something reserved for use by Clojure itself for its own data structures, and that it was a bad idea to mess with it directly.

There are two metatags which carry (officially) type information. One is :tag which is used for type hints used by the compiler. One is :type, which is used to embed type. I don't remember any constraints on using the latter. In the contrary I think it was specifically introduced that people could "bless" their map with a type.

Kind regards
Meikel
 

Chouser

unread,
May 2, 2012, 10:26:23 AM5/2/12
to cloju...@googlegroups.com
Well, thank you all for your input, I appreciate it. Especially Paul's
reminder of how fragile the typedness of a records can be in the face
of dissoc.

I wasn't really arguing for a breaking change, but I guess I was
hoping for an overwhelmingly strong defense of the current behavior.
Instead I'll make do with the understanding that it was a judgement
call, and try to incorporate the behavior into future design
decisions. Maybe there are still more uses for maps+multimethods than
I had been thinking, since that gives you the option of putting the
map type in metadata and therefore not consider it for equality (an
option that doesn't exist with records+protocols). Or perhaps it
would be worth finding a way to easily add map behavior to arbitrary
deftypes, with user-specified equality.

But regardless, I'll be trying to remember these:
- Beware of using records simply for performance as there are real
semantic differences (equality, IFn)
- Remember to be careful with dissoc on records

--Chouser

Christophe Grand

unread,
May 4, 2012, 3:06:56 AM5/4/12
to cloju...@googlegroups.com
Hi

On Wed, May 2, 2012 at 4:26 PM, Chouser <cho...@gmail.com> wrot
But regardless, I'll be trying to remember these:
- Beware of using records simply for performance as there are real
semantic differences (equality, IFn)

Beware of performance too: access is faster with records but I noticed that on records with many (8?) fields, associng is slower than on hashmaps (I guess it's faster to clone an array than to copy N fields).

Structmaps may need a revival.

Christophe
Reply all
Reply to author
Forward
0 new messages