How do I define equality and ordering for my own type, and why can't I write '==' beside '<=>'?
← Operators and Expressions · Ref: Q1402
A type defines its SAMENESS once, and every other comparison is generated from it - two ways to say 'equal' eventually disagree.
CHOOSE ONE
- An ORDERED type writes '<=>'. The compiler generates '==' '<>' '<' '<=' '>' '>=' from it: '==' is (a <=> b) == 0.
- An EQUATABLE-ONLY type (a 2D point: equality means something, ordering does not) writes '=='. '<>' is generated and there is no '<'.
Either can be defaulted instead ('default operator', 'default operator <=>', 'default operator ==').
THE RULES
- Writing, or asking for, any other comparison with the type itself is E07181 - including one taking a super, a trait or Any ('==(Any)').
- '#?' goes with sameness: a hand-written '<=>' or '==' needs a hand-written '#?' hashing what it compares (E07184); defaulted sameness gets a generated '#?'.
- Sameness is declared once per hierarchy: a subtype inherits it and cannot override it (E07182). Any subtype refinement of an ordering is non-transitive once base and subtype values are mixed, so a sort would depend on input order.
- It is concrete where declared (E07183): a trait that orders writes the body over its own abstract accessors, 'rank() <=> other.rank()'.
- A GENERATED '==' or '<=>' between two DIFFERENT concrete classes of one hierarchy (a Leaf and a Middle) is unset; write the root's operator by hand when objects of different classes must compare.
BECAUSE '==' COMES FROM '<=>'
'<=>' defines what makes two values the SAME. Here two Person values with the same id are ==. If '<=>' compared age, two different people of the same age would be == and a Set would keep only one of them.
ANOTHER ORDERING
Sort by anything else with a comparator function: 'sort by byAge'. It never needs a second '<=>'.
This is Java's equals/hashCode/compareTo contract (Effective Java items 10, 11 and 14) made a compile error, with the derived operators generated so they cannot drift.
Example
defines module qa.operators.sameness.once defines function <?- Another ordering is a comparator function given to sort - never a second '<=>'. -?> byAge() as pure -> first as Person second as Person <- rtn as Integer: first.age() <=> second.age() defines class <?- An ORDERED type: '<=>' defines its sameness. '==' '<>' '<' '<=' '>' '>=' are generated from it, and '#?' hashes exactly what '<=>' compares. -?> Person as open id as Integer? name as String? years as Integer? Person() id: Integer() name: String() years: Integer() Person() -> identifier as Integer fullName as String age as Integer id: identifier name: fullName years: age identity() as pure <- rtn as Integer: id age() as pure <- rtn as Integer: years operator <=> as pure -> other as Person <- rtn as Integer: id <=> other.identity() operator #? as pure <- rtn as Integer: #?id operator $ as pure <- rtn as String: name override operator ? as pure <- rtn as Boolean: id? <?- A SUBTYPE inherits its super's sameness and adds none of its own. -?> Student extends Person subject as String: "maths" override operator ? as pure <- rtn as Boolean: identity()? default operator $ <?- An EQUATABLE-ONLY type: '==' defines its sameness, '<>' is generated, and there is no ordering. -?> Point across as Integer? down as Integer? Point() across: Integer() down: Integer() Point() -> x as Integer y as Integer across: x down: y acrossOf() as pure <- rtn as Integer: across downOf() as pure <- rtn as Integer: down operator == as pure -> other as Point <- rtn as Boolean: across == other.acrossOf() and down == other.downOf() operator #? as pure <- rtn as Integer: #?across + #?down override operator ? as pure <- rtn as Boolean: across? and down? defines program SamenessOnce() stdout <- Stdout() ada <- Person(1, "Ada", 36) bob <- Person(2, "Bob", 29) stdout.println(`ada == ada again: ${ada == Person(1, "Ada", 36)}`) stdout.println(`ada < bob: ${ada < bob}`) byIdentity <- cat [bob, ada] | sort | collect as List of Person stdout.println(`by identity: ${byIdentity}`) youngestFirst <- cat [ada, bob] | sort by byAge | collect as List of Person stdout.println(`by age: ${youngestFirst}`) here <- Point(1, 2) stdout.println(`points differ: ${here <> Point(2, 1)}`)
Common mistakes
E07181 — '<' is generated from '<=>', so writing it gives the type a second, disagreeing notion of order. For another ordering, write a comparator function and use 'sort by'.
Incorrect:
operator < as pure -> other as Person <- rtn as Boolean: years < other.age()
Correct:
operator <=> as pure -> other as Person <- rtn as Integer: id <=> other.identity()
E07184 — A hand-written '<=>' decides what is equal, so the hash must be hand-written over the same fields: a defaulted '#?' hashes every field and would give equal values different hashes.
Incorrect:
default operator #?
Correct:
operator #? as pure <- rtn as Integer: #?id
E07182 — A subtype inherits its super's sameness and cannot change it. An ordering by another field belongs in a comparator function.
Incorrect:
subject as String: "maths" override operator <=> as pure -> other as Student <- rtn as Integer: identity() <=> other.identity()
Correct:
subject as String: "maths"
Other ways to ask this
- Why is E07181 reported on my hand-written '<' or '=='?
- Why must '#?' be written when I write '<=>' (E07184)?
- Why can't a subclass override '<=>' or '==' (E07182)?
- How do I sort my type by a different field?
- What is 'sameness' in EK9?
Coming from another language?
Java: equals, hashCode and compareTo are written separately and only conventions and linters (Sonar S1206, S1210, S2162) ask that they agree; subclasses that add a field break the equals contract (Effective Java item 10). Kotlin: data classes generate equals/hashCode but not compareTo, and cannot be extended. C#: IEquatable, GetHashCode and IComparable are separate too. EK9: one '<=>' (or '==') is the single source; the rest are generated, '#?' must agree, and sameness is sealed where it is declared.
Keywords: E07183, E07184, sameness, ==, <>, <=>, sort, default operator, #?, hashcode, equality, comparator, ordering, E07181, E07182