Why are two shapes not equal when my abstract base has 'default operator' and they have the same name?

← Operators and Expressions · Ref: Q2242

A DEFAULTED sameness on an abstract base belongs to each concrete type, not to the hierarchy. 'default operator' on Shape compares only Shape's own fields, so each concrete subtype that says 'default operator' gets its own '<=>' over ALL its fields - the inherited ones first, then its own - and '==' is generated from it. Values of different run-time types are never the same: a Circle and a Square are not '==' and their '<=>' is unset, even when every field they share is equal.

Only a sameness WRITTEN BY HAND on an abstract base or a trait is hierarchy-wide sameness: an ordering over what every subtype provides ('area() <=> other.area()') compares a Circle with a Square, and no subtype may change it (E07182). Choose that when values of different kinds must be compared; default the sameness when each kind is compared only with its own kind.

Example

#!ek9
defines module qa.operators.defaulted.sameness.abstract

  defines constant
    roundName <- "round"

  defines class

    //Defaulted on an abstract base: it compares only 'name', and each concrete type has its own.
    Shape as abstract
      name as String: String()

      Shape()
        -> label as String
        this.name: label

      default operator

    Circle extends Shape
      radius as Integer: Integer()

      Circle()
        ->
          label as String
          size as Integer
        super(label)
        this.radius: size

      default operator

    Square extends Shape
      side as Integer: Integer()

      Square()
        ->
          label as String
          edge as Integer
        super(label)
        this.side: edge

      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

    DefaultedSamenessOnAbstractBase()
      stdout <- Stdout()
      small <- Circle(roundName, 1)
      sameSmall <- Circle(roundName, 1)
      larger <- Circle(roundName, 2)
      square <- Square(roundName, 1)
      stdout.println(`Circle(round, 1) == Circle(round, 1): ${flag(small == sameSmall)}`)
      stdout.println(`Circle(round, 1) == Circle(round, 2): ${flag(small == larger)}`)
      stdout.println(`Circle(round, 1) <=> Circle(round, 2): ${shown(small <=> larger)}`)
      stdout.println(`Circle(round, 1) == Square(round, 1): ${flag(small == square)}`)
      stdout.println(`Circle(round, 1) <=> Square(round, 1): ${shown(small <=> square)}`)
Other ways to ask this
  • Does a 'default operator' on an abstract class compare every subtype the same way?
  • Why is a Circle never equal to a Square when both inherit a defaulted '<=>'?
  • Is a defaulted sameness on an abstract base shared by the whole hierarchy?
  • Why does a subtype that adds a field compare that field too when the base was defaulted?

Coming from another language?

Java: an equals generated on an abstract class (or by Lombok's @EqualsAndHashCode) and inherited by subclasses compares only the superclass fields, unless every subclass regenerates it with callSuper - and a Circle can then equal a Square that happens to share the fields. Kotlin: data classes cannot be extended. EK9: a defaulted sameness is per-type, includes every field, and a different run-time type is never the same.

Keywords: <=>, ==, abstract base, default operator, equality, hierarchy, run-time type, sameness, subtype