What is the difference between hierarchy-wide sameness and per-type sameness, and which one do I have?

← Operators and Expressions · Ref: Q2282

There are two kinds, and which you have depends on how you write it:

HIERARCHY-WIDE sameness is a '<=>' (or '==') written BY HAND on an abstract base or a trait, over what every kind provides (an 'area()'). It compares any two values in the hierarchy, whatever their run-time types, so Square(2.0) == Strip(4.0, 1.0) when both have area 4. It is sealed: no sub-type may declare its own (E07182), so fields a sub-type adds are not compared. Choose it when different kinds must compare, sort and select together.

PER-TYPE sameness is everything else: a concrete type's own '<=>' or '==', or a DEFAULTED one even on an abstract base ('default operator' on Pet compares only Pet's own fields). Each sub-type that adds fields declares its own over all its fields (E07238), and values of different run-time types are never the same: Dog("rex") == Cat("rex") is false and their '<=>' is unset. Choose it when each kind is compared only with its own kind.

A type has one sameness. Another view of it (by value alone, 'aces high') is a comparator function given to 'sort by' (Q2280).

Example

#!ek9
defines module qa.operators.two.kinds.of.sameness

  defines constant
    rexName <- "rex"

  defines class

    <?-
      HIERARCHY-WIDE sameness: '<=>' written by hand on an abstract base, over what every kind provides.
      Every Shape, of any kind, is ordered by area, and no sub-type may change that.
    -?>
    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()
        -> edge as Float
        this.side: edge

      override area() as pure
        <- rtn as Float: side * side

      default operator ?

    Strip extends Shape
      width as Float: 0.0
      height as Float: 0.0

      Strip()
        ->
          across as Float
          up as Float
        this.width: across
        this.height: up

      override area() as pure
        <- rtn as Float: width * height

      default operator ?

    <?-
      PER-TYPE sameness: 'default operator' on an abstract base compares only the base's own fields.
      Each kind has its own, over all its fields, and kinds are never the same as each other.
    -?>
    Pet as abstract
      name as String: String()

      Pet()
        -> petName as String
        this.name: petName

      default operator

    Dog extends Pet
      breed as String: String()

      Dog()
        ->
          petName as String
          kind as String
        super(petName)
        this.breed: kind

      default operator

    Cat extends Pet
      indoor as Boolean: true

      Cat()
        ->
          petName as String
          inside as Boolean
        super(petName)
        this.indoor: inside

      default operator

  defines function

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

  defines program

    TwoKindsOfSameness()
      stdout <- Stdout()

      square <- Square(2.0)
      strip <- Strip(4.0, 1.0)
      stdout.println(`Square(2) == Strip(4, 1), both area 4: ${flag(square == strip)}`)

      rex <- Dog(rexName, "collie")
      tom <- Cat(rexName, true)
      stdout.println(`Dog rex == Cat rex, same name: ${flag(rex == tom)}`)
      stdout.println(`Dog rex == Dog rex: ${flag(rex == Dog(rexName, "collie"))}`)
      stdout.println(`Dog rex == Dog rex, another breed: ${flag(rex == Dog(rexName, "poodle"))}`)

Common mistakes

E07182 — Shape's '<=>' is written by hand on an abstract base: hierarchy-wide sameness, sealed for every sub-type. Square cannot declare or default its own. If each kind should compare only with its own kind, remove the hand-written '<=>' from Shape and default it in each type instead. See ek9 -h E07182.

Incorrect:

<- rtn as Float: side * side

      default operator ?
      default operator <=>

Correct:

<- rtn as Float: side * side

      default operator ?
Other ways to ask this
  • When is a Square equal to a Strip of the same area, and a Dog never equal to a Cat?
  • How do I decide whether the sameness belongs to the whole hierarchy or to each type?
  • Why does 'default operator' on an abstract base not make all its sub-types comparable?
  • Is my sameness hierarchy-wide?

Coming from another language?

Java: equals in a class hierarchy has to choose between instanceof (not symmetric once a subclass adds a field) and getClass (a subclass never equals its super), and that choice is made class by class. EK9: a hand-written sameness on an abstract base or trait is the whole hierarchy's, any other sameness is the type's own, and the compiler enforces the difference.

Keywords: <=>, ==, E07182, E07238, abstract base, default operator, hierarchy-wide, per-type, sameness, trait