#209 Equals/HashCode/Compare

jodastephen Fri 25 Apr 2008

These methods have all been defined on Obj, but I'm not sure that is the right way to go.

A basic coding error is to use a mutable bean as a hashmap key, and then change its value. This occurs because any object may be used as a hashmap key.

Given that fan has explicit const types, I'd suggest it would be appropriate to restrict HashCode to just const types (by default). This could be achieved by a mixin I believe - all const types get the mixin by default, other types can add it manually if they understand the issues.

This same principle extends to equals and compare. Is it really right that every object should have these? Certainly compare makes me worry given that it cause operators like <= to immediately be active for every object.

Equals for simple value objects (almost always const) is usually easy to determine - think of Int, DateTime, String. However, equals for entity/domain classes is much harder to define - Person, Booking, Order.

Often when you compare a domain object you want equals to work on an identifier or primary key (a URL in REST). I'd suggest therefore that adding equals to every object isn't what you want. Nor is compare - I certainly don't want my Person objects to be comparable by default.

Obviously, every object should still support the notion of identity (== in Java).

brian Sat 26 Apr 2008

Map already prevents mutable keys - it will throw an NotImmutableErr if you attempt to use a mutable key.

Currently the notion of immutability isn't built into the type system, because some classes like List, Map, Type, and Func can be both. For example:

fansh> Int[] x := [0, 1, 2]
[0, 1, 2]
fansh> Int[] y := x.toImmutable
[0, 1, 2]
fansh> x.isImmutable
fansh> y.isImmutable

Both x and y are typed as Int[], but their immutability is a runtime issue. That isn't necessarily perfect, but it comes back to the sophistication of the type system versus simplicity. Fan uses a fairly limited, but simple type system and relies on runtime checks for things like immutable checking.

I agree that object identity is difficult, and not widely implemented correctly. One thing I did was have Test.verifyEq check hash as well as equals. So unless you skip unit testing, that problem should be flushed out. As long as you don't override hash and equals, then the default implementation is reference equality and that is typically always safe.

Regarding compare - on ever Java project I've worked on, I've always had a utility to sort based on Comparable or if not implemented than to sort based on String (and it gets used a whole lot). Say I'm displaying a bunch of stuff in a table (often just for debug), I always want sorting just like I always want a string representation. That is why Obj.compare defaults to sort based on the toStr representation.

I think you are framing your question as do these methods belong on every Obj? And I've framed the question can we provide sensible defaults for these methods? If you override them, then obviously you have to know what you are doing and all three need to be overridden together.

helium Sat 26 Apr 2008

OK, lets imagine I implement a simple complex number class. Now What should the order be? Is 2 + 1i > 1 + 2i? Or vice versa? In mathmatics there is no ordering of complex numbers defined. If this isn't even possible for simple numbers how can I implement ordering for a lot more complex objects? Let's take a class moddling an athlete. Should I order athletes by their personal records or should I order them by their surname or by their age or ...? Is it usefull if they are ordered by their string representation which might be something like "forename surname ...".

It depends very much on the situation. So normaly I want to express this my self.

athletes.sort | Athlete a, Athlete b | { return a.surename <=> b.surename }


athletes.sort | Athlete a, Athlete b | { return -(a.personalRecord <=> b.personalRecord) }

helium Sat 26 Apr 2008

Regarding const / not const in the type system ... well, while I favor more powerfull type systems I absolutly agree that this is nothing that should be in fan.

In order to model this you'd have to introduce a kind system (kinds are somthing like types of types for those who don't know them). You could have two kinds: The kind * of normal types and another kind C of constant types. And then there would be a type-level function from * to C to generate the constant types coresponding to the normal mutable types.

And than you'd have to adapt your existing type-level functions. You'd have to change Map from

(*, *) -> *


(C, *) -> *

Hmmm, ... Now that I see this it would get even more complicated. You should be able to store constant values in maps as well, so C would have to be a subkind of * or you'd have to find some other way to express this (overloading Map, List, ...?). Or perhaps I'm just making some error in my train of thought.

Anyway, please don'T introduce the trouble of Java's kind system with its two kinds PrimitiveType and ReferenceType. While "Array" is somehow overloaded for PrimitiveType -> ReferenceType as well as ReferenceType -> ReferenceType, user defined type-level functions are allways ReferenceType -> ReferenceType (or with more arguments), so you have to use wrappers, etc.

jodastephen Sun 27 Apr 2008

Brian, I guess I'd contrast your use case of always needing a string based compare method, with my experience of pretty much never needing one like that.

When I write a compare method its almost always for a specific reason - comparing by price, or priority, or importance. As such, I'd feel much happier having comparison as an unimplemented mixin.

More generally, I'm happy that maps reject mutable keys, and reasonably conformtable with the idea of this as a runtime check.

Login or Signup to reply.