Why is a Holder never equal to a Named that extends it, even with the same count?

← Operators and Expressions · Ref: Q2243

Values of different run-time types are never the same in EK9. Per-type sameness - the usual kind - is the type's own: '==' between a Holder and a Named is false, '<>' is true and '<=>' is UNSET - EK9's answer for 'not comparable' - whatever fields they share. The compiler puts the run-time type check in front of the comparison, so the author only writes the same-type case.

The OBJECT decides which comparison runs, never the variable: 'held as Holder: Named(1, "x")' compared with another Named runs Named's '==', over all of Named's fields.

So collections keep different kinds apart: 'contains' does not find a Named by an equal-looking Holder, and a Dict keyed by Holder(2) and Named(2, ...) holds two entries.

To compare values of different kinds deliberately, give their ABSTRACT base or trait a hand-written sameness over what every kind provides (every Shape by area) - that one is hierarchy-wide sameness.

Example

#!ek9
defines module qa.operators.different.types.never.same

  defines constant
    firstName <- "x"

  defines class

    Holder as open
      count as Integer: 1

      Holder()
        -> start as Integer
        this.count: start

      default operator

    Named extends Holder
      name as String: String()

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

      default operator

  defines function

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

    shown()
      -> comparison as Integer
      <- rtn as String: comparison? <- $comparison else "unset"

  defines program

    DifferentTypesNeverTheSame()
      stdout <- Stdout()
      plain <- Holder(2)
      named <- Named(2, firstName)
      stdout.println(`Holder(2) == Named(2, x): ${flag(plain == named)}`)
      stdout.println(`Holder(2) <> Named(2, x): ${flag(plain <> named)}`)
      stdout.println(`Holder(2) <=> Named(2, x): ${shown(plain <=> named)}`)

      //The object decides: held is a Named, so Named's '==' runs.
      held as Holder: Named(2, firstName)
      stdout.println(`held == Named(2, x): ${flag(held == named)}`)
      stdout.println(`held == Named(2, y): ${flag(held == Named(2, "y"))}`)

      holders as List of Holder: [named]
      stdout.println(`contains Holder(2): ${flag(holders contains plain)}`)

      counts as Dict of (Holder, String): {plain: "plain"}
      newEntry as DictEntry of (Holder, String): DictEntry(held, "named")
      counts += newEntry
      stdout.println(`entries: ${length counts}`)
Other ways to ask this
  • Why does '<=>' between a class and its subclass give unset?
  • Can a value of a subtype be equal to a value of its super type in EK9?
  • Why does 'contains' not find a subtype value with the same fields?
  • Why does a Dict keep a Holder key and a Named key with the same count apart?
  • Which '==' runs when I compare through a super-typed variable?

Coming from another language?

Java: equals(Object) in a class hierarchy has to choose between instanceof (not symmetric once a subclass adds a field) and getClass() == o.getClass() (Effective Java item 10); compareTo has no answer for 'not comparable' and must throw. Kotlin: the same choice in an open class's equals. EK9: the getClass() rule is the language's, generated for you, and '<=>' answers unset instead of throwing.

Keywords: <=>, ==, Dict key, contains, equality, polymorphism, run-time type, sameness, subtype, unset