How does 'sort' order a list that holds a class and its subclass, and where do unset values go?

← Streams and Pipelines · Ref: Q2246

Values of different run-time types are never comparable in EK9 - their '<=>' is unset - so 'sort' cannot interleave them. It must still place every element, so it GROUPS them: by run-time type name, each group in the order of that type's own '<=>', and every unset element last, in the order it arrived. Here every Holder comes before every Named ('Holder' sorts before 'Named'), the Holders by count, the Nameds by count and then name, and the unset Holder at the end.

To interleave different kinds, give their ABSTRACT base or trait one hand-written sameness over what every kind provides (every Shape by area): that ordering is hierarchy-wide, so any two values compare (it must answer for every pair of set values: two values of one type that it leaves unset stop the sort with an EK9 Exception naming them). A built-in whose '<=>' holds only within a group of its own values sorts GROUPED the same way: Money by currency and Dimension by unit suffix, each group in order, the groups in code order ([10#GBP, 5#USD, 3#GBP] sorts to 3#GBP 10#GBP 5#USD). A value that cannot be compared with itself is kept with the unset ones. For any other order, sort with a comparator: 'sort by byCount' - it too must answer for every pair of set values, and one that answers unset stops the sort with the same Exception (Q2283).

Unset elements are kept by 'sort' and 'group': after a sort, 'group' puts every unset element into ONE final group, in input order, after the groups of set values. 'collect as List' keeps them too - a stream stage never drops an unset element, and a terminal that writes into a List appends with '+=' - so 'sort | collect as List of Holder' holds every element, the unset one last. Collecting into a Set, a Dict or an Optional, or piping into a List with its own '|', still ignores an unset element, and a 'filter' on the stream is how you remove them.

Example

#!ek9
defines module qa.streams.sort.mixed.types

  defines constant
    firstName <- "ada"
    secondName <- "bob"

  defines class

    Holder as open
      count as Integer: Integer()

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

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

      operator $ as pure
        <- rtn as String: `Holder(${count})`

      default operator <=>
      default operator #?
      default operator ?

    Named extends Holder
      name as String: String()

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

      override operator $ as pure
        <- rtn as String: `Named(${tally()}, ${name})`

      default operator <=>
      default operator #?
      default operator ?

  defines function

    shown()
      -> held as Holder
      <- rtn as String: held? <- $held else "unset"

  defines program

    SortMixedTypes()
      stdout <- Stdout()
      holders as List of Holder: [Named(2, secondName), Holder(3), Holder(), Holder(1), Named(1, firstName)]
      cat holders | sort | map with shown > stdout
Other ways to ask this
  • Why does sort put all the Holders before all the Nameds?
  • How does EK9 sort values of different run-time types?
  • Where does sort put unset elements?
  • Why are my subclass values not interleaved with the super class values after a sort?

Coming from another language?

Java: Collections.sort over a List<Holder> uses the super class's compareTo for every element, so a subclass that refines equality is 'inconsistent with equals'; a compareTo that throws for an unrelated subtype makes the sort throw. Kotlin: the same. EK9: '<=>' answers unset for different run-time types, and sort groups by type instead of failing.

Keywords: <=>, comparator, mixed types, run-time type, sameness, sort, stream, subtype, unset