Why can't I use '<?' on my open class (E07187)?

← Operators and Expressions · Ref: Q2247

'<?' and '>?' select one operand by comparing the two with '<=>'. On an open class, an abstract class, an open or abstract record or component, or a trait the two values can be of DIFFERENT run-time types - a Score and a Bonus that extends it - and two values of different run-time types are never in order: their '<=>' is UNSET. Then there is no lesser or greater value to select, and an unset value cannot be built for your own type to stand in for one, so the operators are E07187 on such a type.

Three ways out:

1. Make the class closed (remove 'as open'): two Score values are then always of one run-time type and always compare.
2. Order the whole hierarchy: write '<=>' BY HAND on its abstract base or trait, over what every subtype provides - every Shape by area - with a hand-written '#?' beside it. That '<=>' orders any two values, so '<?' can always choose (here a Square and a Circle).
3. Keep the hierarchy, compare with '<=>' yourself and decide what an unset result means.

A generic that uses these operators on its T is checked where T is given its type: 'lesserOf() of Score' is fine for a closed Score and E07187 for an open one. Closed classes, records and components, enumerations, constrained types and the built-in types are never affected.

Example

#!ek9
defines module qa.operators.coalescing.comparison.open.class

  defines class

    //Closed: two Score values are always of one run-time type
    Score
      points as Integer: Integer()

      Score() as pure
        -> start as Integer
        points :=? start

      default operator

    //A '<=>' written by hand on an abstract base orders the whole hierarchy
    Shape as abstract
      area() as pure abstract
        <- rtn as Integer?

      operator <=> as pure
        -> other as Shape
        <- rtn as Integer: area() <=> other.area()

      operator #? as pure
        <- rtn as Integer: #? area()

    Square extends Shape
      side as Integer: Integer()

      Square() as pure
        -> start as Integer
        side :=? start

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

      override operator ? as pure
        <- rtn as Boolean: side?

    Circle extends Shape
      radius as Integer: Integer()

      Circle() as pure
        -> start as Integer
        radius :=? start

      override area() as pure
        <- rtn as Integer: radius * radius * 3

      override operator ? as pure
        <- rtn as Boolean: radius?

    //Open, with a defaulted sameness: '<?' is E07187 here, '<=>' is not
    Animal as open
      name as String: String()

      Animal() as pure
        -> initial as String
        name :=? initial

      default operator

    Cat extends Animal
      Cat() as pure
        -> initial as String
        super(initial)

  defines function

    lesserOf() of type T
      ->
        first as T
        second as T
      <- rtn as T: first <? second

  defines program

    OneOrder()
      stdout <- Stdout()

      first <- Score(9)
      second <- Score(7)
      stdout.println(`lower score: ${first <? second}`)
      stdout.println(`higher score: ${first >? second}`)

      square as Shape: Square(2)
      circle as Shape: Circle(1)
      smaller <- square <? circle
      larger <- square >? circle
      stdout.println(`smaller shape area: ${smaller.area()}`)
      stdout.println(`larger shape area: ${larger.area()}`)

      scoreLesser <- lesserOf() of Score
      stdout.println(`lesser of two scores: ${scoreLesser(first, second)}`)

      //Unset when the two are of different run-time types: keep the first then
      pet as Animal: Animal("cat")
      other as Animal: Cat("ant")
      comparison <- pet <=> other
      kept <- comparison? and comparison > 0 <- other : pet
      stdout.println(`kept: ${kept}`)

Common mistakes

E07187 — Score is open, so two Score values can be of different run-time types, whose '<=>' is unset: there may be no lesser one to select. Make Score closed, write '<=>' by hand on an abstract base or trait, or compare with '<=>' and handle unset. See ek9 -h E07187.

Incorrect:

    //Open: a Score and a subclass of it would be different run-time types
    Score as open

Correct:

    //Closed: two Score values are always of one run-time type
    Score
Other ways to ask this
  • What does E07187 mean?
  • Why is '>?' rejected on an abstract class with 'default operator'?
  • Why can't I take the lesser of two values of a base class with '<?'?
  • Why does a generic min helper fail when I give it the base class of a hierarchy?
  • How do I pick the smaller of two shapes of different kinds?

Coming from another language?

Java: Math.min only takes numbers; for objects you write compareTo and choose, and a compareTo between a base and a subclass either throws ClassCastException or silently compares only the base's fields. Kotlin's minOf(a, b) over Comparable has the same problem. EK9 answers 'not comparable' with an unset '<=>' and refuses a selection that could have no answer at compile time.

Keywords: <?, >?, E07187, abstract class, coalescing, comparison, maximum, minimum, open class, run-time type, sameness