E07188: a '<=>' must order every two set values of its type: it cannot return an unset Integer, nor build its result on a '<=>' (a field's, a super's, a function's) that answers unset for two set values

Category: E07xxx Full resolution · Reported in phase: PRE_IR_CHECKS · Classification: PARTIAL_COMPARISON_OPERATOR · All error codes

What went wrong

A '<=>' written by hand can leave its result UNSET for two values of its own type that are both SET. The type then has no order between those two values: a sort over a list of them would depend on the order the list arrived in, and '<?' or '>?' would hand back whichever operand happened to come second.

EK9 checks this when the operator is declared, because the compiler knows every type that can reach it. A '<=>' must order every two SET values of its type. It is reported when the Integer it returns can come from

  • an unset Integer: 'Integer()', typically the 'else Integer()' of a '<=>' that only compares values of one unit;
  • a '<=>' that is itself partial: the '<=>' of Money (defined only within one currency) or Dimension (only within one unit suffix), the hand-written '<=>' of another type that is partial, or a method or function whose result comes from one of these;
  • the '<=>' of a type whose values can be of DIFFERENT run-time types: an open or abstract class, record or component, or a trait, whose sameness is per-type, not hierarchy-wide (one written by hand on an abstract base or trait). Two values of different run-time types are never the same, so their '<=>' is unset. This holds whether or not a sub-type exists yet: another module can add one.

A sub-type that overrides a '<=>' with one that can answer unset is reported on the sub-type's own '<=>', not on the super's.

The check looks at where the returned Integer can get its value, not at the path the code takes: a partial source is reported even when a test guards it. A path that never assigns a returning variable declared with no value is E08050. A '<=>' that is generated ('default operator <=>') is never reported here: over a field that is partial (Money, Dimension, or an open or abstract type as above) it is withdrawn, and a use of it is E07620.

Worked examples

The [EXAMPLE] section below shows a Length that returns an unset Integer for two lengths in different units, and a Priced that compares Money values directly, then the fixes - compare the unit first and the amount second, and compare a Money's currency (its '#>') before its amount (its '#<').

How to fix it

  1. Make the result total: compare the part that groups the values (the unit, the currency) first, then the part that orders within a group. 'rtn: unit <=> other.unit' and then 'if rtn == 0' compare the amounts.
  2. For Money compare '#>' (the currency, a String) and then '#<' (the amount, a Float). For a Dimension the same two prefix operators give the suffix and the value.
  3. Never write 'Integer()' as the result of a '<=>'. Declare the result with no value ('<- rtn as Integer?') and let E08050 prove that every path assigns it.
  4. For a field of an open or abstract type: put the ordering on an abstract base or a trait, written by hand over what every sub-type provides ('area() <=> other.area()'), so it is hierarchy-wide sameness; or make the field's type closed (remove 'as open'); or compare a part of the field that every run-time type has ('item.size()').
  5. If the type should not be ordered at all, declare 'default operator ==' and compare with a comparator function.

Common causes

  • 'else Integer()' on a '<=>' that compares only values of the same unit, kind or currency
  • Comparing two Money or Dimension fields with '<=>' directly
  • A '<=>' that calls a helper whose result is unset for some pair of values
  • Comparing a field whose type is open (or abstract with a defaulted '<=>'): a value of the type and one of a sub-type compare unset

See also

  • E07620: a use of an ordering that is not available (a defaulted '<=>' over a Money, Dimension or open-typed field is withdrawn)
  • E07187: '<?' and '>?' over a type whose values can be of different run-time types
  • E07200: 'default operator <=>' written out over a field that is not totally ordered
  • E08050: a returning variable that is not assigned on every path
  • E07181: comparisons generated from the sameness

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E07188: a '<=>' must order every two set values of its type: it cannot return an unset Integer, nor build its result on a '<=>' (a field's, a super's, a function's) that answers unset for two set values

#!ek9
defines module error.e07188.bad

  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

      //Two lengths in different units are set, but this '<=>' gives them no order: it returns an unset Integer
      operator <=> as pure
        -> other as Length
        <- rtn as Integer: unit == other.measure() <- amount <=> other.quantity() else Integer()

      default operator ?

      default operator $

      default operator $$

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

      default operator :=:

    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 one currency: 10 GBP <=> 5 USD is unset, and so is this '<=>'
      operator <=> as pure
        -> other as Priced
        <- rtn as Integer: price <=> other.price

      default operator ?

      default operator $

      default operator $$

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

      default operator :=:

Fixed: Order every two set values: compare the unit or currency first, then the amount

#!ek9
defines module error.e07188.good

  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()

      default operator ?

      default operator $

      default operator $$

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

      default operator :=:

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

      Priced()
        ->
          name as String
          price as Money

        this.name: name
        this.price: price

      //The currency (String) is compared first, then the amount (Float): both orders are total
      operator <=> as pure
        -> other as Priced
        <- rtn as Integer: #> price <=> #> other.price
        if rtn == 0
          rtn: #< price <=> #< other.price

      default operator ?

      default operator $

      default operator $$

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

      default operator :=:

Questions and answers

These Q&A entries show E07188 and its fix: