Why is my hand-written '<=>' an error because it can answer unset (E07188)?

← Operators and Expressions · Ref: Q2262

A '<=>' must put every two SET values of its type in order. A type's sameness is declared once and everything else is generated from it - '<', '<=', '>', '>=', '<?', '>?', 'sort', a range, a sorted queue - so a '<=>' that answers UNSET for two set values would make all of them depend on which operand came first or which way the list arrived. The compiler knows every type that can reach the operator, so it checks the declaration instead of leaving the failure for a run.

E07188 is reported when the Integer a hand-written '<=>' returns can come from an unset Integer ('else Integer()') or from a '<=>' that is itself partial: Money's (defined only within one currency), Dimension's (only within one unit suffix), another type's hand-written one that is partial, or a helper that returns one. The check looks at where the result can come from, not at the path taken, so a guarded partial source is still reported.

The fix is to compare the part that groups the values first, then the part that orders inside a group: unit, then amount; for Money, '#>' (its currency, a String) then '#<' (its amount, a Float). Money and Dimension are the only built-in types with a partial '<=>', and they say so themselves. Their selection operators give an UNSET value across currencies or units, and 'sort' puts each currency or unit in a group of its own, the groups in code order.

A '<=>' you only ask for ('default operator <=>' or the bare 'default operator') over a Money or Dimension field is withdrawn rather than reported: the type keeps '==', and using its ordering is E07620 (an explicit 'default operator <=>' there is E07200).

Example

#!ek9
defines module qa.operators.comparison.must.order.every.pair

  defines class

    Length
      amount as Integer: Integer()
      unit as String: String()

      Length()
        ->
          amount as Integer
          unit as String
        this.amount: amount
        this.unit: unit

      measure() as pure
        <- rtn as String: unit

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

      //Every pair of lengths is ordered: by unit first, then by amount - no path returns an unset Integer
      operator <=> as pure
        -> other as Length
        <- rtn as Integer: unit <=> other.measure()
        if rtn == 0
          rtn: amount <=> other.quantity()

      operator #? as pure
        <- rtn as Integer: #? amount + #? unit

      default operator ?

      operator $ as pure
        <- rtn as String: `${amount}${unit}`

    Priced
      name as String: String()
      price as Money: Money()

      Priced()
        ->
          name as String
          price as Money
        this.name: name
        this.price: price

      //Money orders only within a currency, so the currency (a String) is compared first, then the amount (a Float)
      operator <=> as pure
        -> other as Priced
        <- rtn as Integer: #> price <=> #> other.price
        if rtn == 0
          rtn: #< price <=> #< other.price

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

      default operator ?

      operator $ as pure
        <- rtn as String: `${name}:${price}`

  defines function

    shown()
      -> item as Money
      <- rtn as String: item? <- $item else "unset"

  defines program

    ShowOrdering()
      stdout <- Stdout()
      lengths <- cat [Length(7, "m"), Length(3, "ft"), Length(5, "m")] | sort | collect as List of Length
      stdout.println(`lengths: ${lengths}`)
      stdout.println(`smaller: ${Length(3, "ft") <? Length(5, "m")}`)

      priced <- cat [Priced("a", 10#GBP), Priced("b", 5#USD)] | sort | collect as List of Priced
      stdout.println(`priced: ${priced}`)

      //Money itself is partial: within a currency it selects, across currencies there is no answer
      pounds <- 10#GBP
      cheaper <- 3#GBP
      dollars <- 5#USD
      stdout.println(`cheapest within a currency: ${shown(pounds <? cheaper)}`)
      stdout.println(`across currencies: ${shown(pounds <? dollars)}`)

Common mistakes

E07188 — Two lengths in different units are both set, but this '<=>' returns an unset Integer for them, so they have no order and a sort or '<?' would depend on the input. Compare the unit first, then the amount, so every pair is ordered. See ek9 -h E07188.

Incorrect:

        <- rtn as Integer: unit == other.measure() <- amount <=> other.quantity() else Integer()

Correct:

        <- rtn as Integer: unit <=> other.measure()
        if rtn == 0
          rtn: amount <=> other.quantity()
Other ways to ask this
  • What does E07188 mean?
  • Why can't my '<=>' return Integer() when two values are in different units?
  • Why is comparing two Money fields with '<=>' inside my own '<=>' rejected?
  • How do I order values of one type that come in different currencies or units?
  • Why does a defaulted '<=>' over a Money field give E07620 when I sort or compare?

Coming from another language?

Java: a compareTo that returns 0 or throws for incomparable values is allowed, and Collections.sort or a TreeSet then misbehaves - or throws 'Comparison method violates its general contract!' - only at run time, and only for some inputs. Comparing BigDecimal amounts of different currencies is left to the programmer. EK9 makes 'a comparison orders every pair' a compile-time rule, and the types whose order is really per group (Money, Dimension) declare it.

Keywords: <=>, Dimension, E07188, E07620, Money, comparison, currency, sameness, sort, total order, unset