Why were two of my subclass objects equal when only the field the subclass added was different (E07238)?

← Operators and Expressions · Ref: Q2248

Because the sameness they used was the super class's, written for the super class's fields. A subclass that adds a field and declares no '<=>' (or '==') of its own would inherit the super's, and its '#?' with it: both look only at the super's fields, so Named(1, "ada") and Named(1, "bob") were '==', '<=>' gave 0 and their hashes were equal. Nothing at run time can catch it - both values are Named, so the check that values of different run-time types are never the same does not apply.

So a type that adds fields, concrete or abstract, declares its own sameness wherever it would inherit one, as it already declares its own '?', '$', '$$' and ':=:'. Declaring none is E07238, and the message names every operator the type would inherit, in one error.

The fix is one line:
- 'default operator <=>' below an ordered super (one with '<=>') - it compares the super's '<=>' first, then the new fields, and generates '#?' too;
- 'default operator ==' below an equatable-only super (one with '==' and no '<=>');
- or the bare 'default operator', which declares all of them and gives the same kind of sameness as the super;
- or a hand-written '<=>' with a hand-written 'override operator #?' over the same fields.

A subclass that adds NO fields needs nothing: the inherited sameness already covers everything it holds. And a sameness WRITTEN BY HAND on an abstract base or a trait is hierarchy-wide sameness (every Shape by area), so its subtypes add fields and declare none (E07182). An abstract type that adds fields therefore chooses: 'default operator' gives it a per-type sameness (each concrete subtype that adds fields declares its own again), while a '<=>' written by hand on it is hierarchy-wide sameness, sealed for every subtype.

Example

#!ek9
defines module qa.operators.subtype.own.sameness

  defines constant
    firstName <- "ada"
    secondName <- "bob"

  defines class

    //An ordered type: the bare 'default operator' gives it '<=>' over id, and '#?'.
    Account as open
      id as Integer: 1

      Account()
        -> start as Integer
        this.id: start

      default operator

    //Adds a field, so it declares its own sameness: Account's '<=>' first, then name.
    Named extends Account
      name as String: String()

      Named()
        ->
          start as Integer
          label as String
        super(start)
        this.name: label

      default operator ?
      default operator $
      default operator $$
      default operator :=:
      default operator <=>

    //Adds no field: Account's sameness already covers everything it holds.
    Plain extends Account
      Plain()
        -> start as Integer
        super(start)

  defines function

    flag()
      -> answer as Boolean
      <- rtn as String: answer? <- $answer else "unset"

  defines program

    SubtypeOwnSameness()
      stdout <- Stdout()
      ada <- Named(1, firstName)
      bob <- Named(1, secondName)
      stdout.println(`Named(1, ada) == Named(1, bob): ${flag(ada == bob)}`)
      stdout.println(`Named(1, ada) <=> Named(1, bob): ${ada <=> bob}`)
      stdout.println(`same hash: ${flag((#? ada) == (#? bob))}`)

      viaAccount as Account: bob
      stdout.println(`through an Account reference: ${flag(viaAccount == ada)}`)
      stdout.println(`Plain(1) == Plain(1): ${flag(Plain(1) == Plain(1))}`)

Common mistakes

E07238 — Named adds 'name' but declares no sameness, so it would inherit Account's '<=>' and '#?', which compare and hash only the id: Named(1, "ada") and Named(1, "bob") would be the same. Declare 'default operator <=>' (it compares Account's first, then name, and gives '#?' too) or the bare 'default operator'. See ek9 -h E07238.

Incorrect:

      default operator :=:

Correct:

      default operator :=:
      default operator <=>
Other ways to ask this
  • Why does E07238 say my subclass would inherit '<=>' and '#?'?
  • Why must a subclass that adds a field declare its own '<=>' or '=='?
  • Do I need 'default operator <=>' on a subclass when its super class already has one?
  • Why do two objects of my subclass with different names have the same hash code?

Coming from another language?

Java: a subclass that adds a field and inherits equals, hashCode and compareTo silently compares only the super's fields (Effective Java, 3rd ed., Item 10: there is no way to extend an instantiable class and add a value component while preserving the equals contract). Kotlin data classes cannot be extended at all. EK9: the subclass must declare its own sameness, and 'default operator <=>' makes that one line that compares the super's fields first.

Keywords: #?, <=>, ==, E07238, adds fields, default operator, inheritance, sameness, subclass