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