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