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 '#?'.
- Two kinds of sameness. HIERARCHY-WIDE sameness is written BY HAND on an ABSTRACT base or a TRAIT: it is how values of different kinds compare (every Shape by area), and every subtype inherits it and cannot change it (E07182). PER-TYPE sameness is everything else - a concrete type's own, or a DEFAULTED one even on an abstract base, which compares only the base's fields. An abstract type that adds fields below a sameness declares its own too (E07238): defaulted for a per-type one, by hand for a hierarchy-wide one. A subtype refinement of a hierarchy-wide ordering is non-transitive once values of different subtypes are mixed, so a sort would depend on input order.
- Per-type sameness is the type's own: a subtype may declare or default its own, over all its fields - and one that ADDS fields must (E07238), since the inherited one cannot know them - and values of different run-time types are never the same - '==' is false and '<=>' unset. The compiler checks the run-time type first, so the author only writes the same-type comparison.
- It has a body where it is declared (E07183): a trait that orders writes the body over its own abstract accessors, 'rank() <=> other.rank()'.
- 'sort' over values of different run-time types groups them by type name, each group by its own '<=>', unset values last. To order different kinds against each other, give their abstract base or trait a hierarchy-wide sameness.
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
#!ek9 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 that adds fields declares its own sameness (E07238): Student's defaulted '<=>' compares Person's first (the id), then the subject. A Student and a Person are never '==': they are different run-time types. -?> Student extends Person subject as String: "maths" override operator ? as pure <- rtn as Boolean: identity()? default operator $ 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? <?- A HAND-WRITTEN sameness on an abstract base is hierarchy-wide: every Shape, of any kind, is ordered by area. -?> Shape as abstract area() as pure abstract <- rtn as Float? operator <=> as pure -> other as Shape <- rtn as Integer: area() <=> other.area() operator #? as pure <- rtn as Integer: #? area() Square extends Shape side as Float: 0.0 Square() -> sideLength as Float this.side: sideLength override area() as pure <- rtn as Float: side * side default operator ? 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 — Shape is abstract and orders every shape, of any kind, by area: that sameness is hierarchy-wide, so a Square cannot add its own. An ordering by another field belongs in a comparator function.
Incorrect:
override area() as pure <- rtn as Float: side * side default operator <=>
Correct:
override area() as pure <- rtn as Float: side * side
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 subtype of an abstract base or a trait change its '<=>' (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, a hand-written sameness on an abstract base or trait is hierarchy-wide, and values of different run-time types are never the same.
Keywords: #?, <=>, <>, ==, E07181, E07182, E07183, E07184, comparator, default operator, equality, hashcode, ordering, sameness, sort