Why does my class get only '==' and no '<' when one of its fields is an open class (E07620, E07188)?

← Operators and Expressions · Ref: Q2281

An open (or abstract) class can be extended, so a field of that type may hold a value of the class or of any sub-type, and two values of different run-time types are never in order: their '<=>' is unset. A '<=>' over such a field would answer unset for two set values, and then '<', 'sort' and '<?' would depend on which operand came first or how the list arrived.

So EK9 does not guess:
- the bare 'default operator' leaves the '<=>' out. The class keeps '==' and '<>', field by field, and using '<', '<=', '>', '>=', 'sort' or '<?' on it is E07620;
- a '<=>' you write by hand over that field ('item <=> other.item') is E07188, reported at the declaration;
- an explicit 'default operator <=>' over it is E07200.

The same happens with a Money or Dimension field (Q2262), which only order within one currency or unit.

Your choices:
1. Keep the equality and state the order you want as a comparator function: 'sort by byItem'. It compares a part every kind has (here the label), so it always answers.
2. Write the class's '<=>' by hand over such a part ('itemLabel() <=> other.itemLabel()', with a hand-written '#?').
3. Make the field's class closed (remove 'as open') if nothing needs to extend it, or give its abstract base or trait a hand-written '<=>' (hierarchy-wide sameness, Q2243): then any two values compare.

Example

#!ek9
defines module qa.operators.open.field.ordering

  defines class

    <?-
      An open class: a value of it may really be a sub-type, and values of different run-time types have no order.
    -?>
    Product as open
      name as String: String()

      Product()
        -> productName as String
        this.name: productName

      label() as pure
        <- rtn as String: name

      default operator

    <?-
      The bare 'default operator' cannot order a Product field, so Entry gets '==' (field by field) and no '<=>'.
    -?>
    Entry
      item as Product: Product()
      count as Integer: 0

      Entry()
        ->
          item as Product
          count as Integer
        this.item: item
        this.count: count

      itemLabel() as pure
        <- rtn as String: item.label()

      quantity() as pure
        <- rtn as Integer: count

      default operator

  defines function

    <?-
      The ordering you want is yours to state, with a comparator function: by the label of the item.
    -?>
    byItem() as pure
      ->
        first as Entry
        second as Entry
      <- rtn as Integer: first.itemLabel() <=> second.itemLabel()

  defines program

    OpenFieldOrdering()
      stdout <- Stdout()

      pens <- Entry(Product("pen"), 3)
      samePens <- Entry(Product("pen"), 3)
      paper <- Entry(Product("paper"), 1)
      stdout.println(`same entry: ${pens == samePens}`)
      stdout.println(`different entry: ${pens == paper}`)

      entries <- cat [pens, paper] | sort by byItem | collect as List of Entry
      for entry in entries
        stdout.println(`by item: ${entry.itemLabel()}`)

Common mistakes

E07620 — Entry has a field of the open class Product, so its bare 'default operator' gives it '==' but no '<=>': '<' is not available on it. Compare with a comparator function (sort by byItem) or write '<=>' by hand over a part every Product has. See ek9 -h E07620.

Incorrect:

      stdout.println(`less: ${pens < paper}`)
      entries <- cat [pens, paper] | sort by byItem | collect as List of Entry

Correct:

      entries <- cat [pens, paper] | sort by byItem | collect as List of Entry

E07188 — Product is open, so two items of different run-time types have an unset '<=>', and Entry's '<=>' built on it can answer unset for two set Entry values. Compare a part every kind has (the label), as the comparator byItem does, instead. See ek9 -h E07188.

Incorrect:

      quantity() as pure
        <- rtn as Integer: count

      operator <=> as pure
        -> other as Entry
        <- rtn as Integer: item <=> other.item

      operator #? as pure
        <- rtn as Integer: #? item

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

Correct:

      quantity() as pure
        <- rtn as Integer: count

      default operator
Other ways to ask this
  • Why is '<' not available on a class that holds a field of an open type?
  • Why is my hand-written '<=>' over a field of an open class refused (E07188)?
  • Why does the bare 'default operator' leave out '<=>' when a field type can be extended?
  • How do I sort objects that hold a field of an open or abstract type?

Coming from another language?

Java: a compareTo over a field of a non-final class compiles, then misbehaves or throws only when two sub-types meet at run time. EK9 decides this when the class is compiled: the ordering is withdrawn (E07620) or refused (E07188), and the equality remains.

Keywords: <=>, E07188, E07200, E07620, comparator, default operator, field, open class, sameness, sort by