How do comparison operators work between constrained types and their base types?

← Advanced Type System · Ref: Q717

Constrained types inherit comparison operators from the base type with dual signatures: each operator works with both the constrained type and the raw base type.

DUAL SIGNATURE PATTERN

Every comparison operator gets two forms:

  isEqual1 <- age1 == age2     //DrivingAge == DrivingAge -> Boolean
  isEqual2 <- age1 == 25       //DrivingAge == Integer -> Boolean

This means you can compare constrained values directly with literals.

WHY DUAL SIGNATURES MATTER

Without dual signatures, you would need to wrap every literal:

  isEqual <- age1 == DrivingAge(25)    //works but verbose
  isEqual <- age1 == 25                //also works — much cleaner

The compiler generates both forms automatically.

ALL COMPARISON OPERATORS

The full set of inherited comparisons (when base type has them):

  ==     equality
  <>     not equal
  <      less than
  <=     less or equal
  >      greater than
  >=     greater or equal
  <=>    spaceship (returns Integer)

OPERATORS FROM BASE TYPE ONLY

If the base type lacks an operator, the constrained type does not have it either. A record with only == and <> will not support <, <=, >, >= on its constrained type.

CONTAINS AND MATCHES

String-constrained types inherit contains and matches:

  hasAlice <- name1 contains "Alice"    //String has contains
  matched <- name1 matches /^[A-Z]/     //String has matches

Integer-constrained types do NOT have these.

See Q257 for constrained type overview. See Q716 for arithmetic operators. See Q718 for the promote operator.

Example

defines module qa.advancedtypes.constrainedcomparisons

  defines type

    DrivingAge as Integer constrain as
      >= 16 and <= 100

    Name as String constrain as
      matches /^[a-zA-Z -]+$/

  defines program

    ConstrainedComparisonDemo()
      stdout <- Stdout()

      // === EQUALITY: CT == CT and CT == BaseType ===
      age1 <- DrivingAge(25)
      age2 <- DrivingAge(30)

      isEqual <- age1 == age2
      stdout.println(`25 == 30: ${isEqual}`)

      twentyFive <- 25
      isEqualBase <- age1 == twentyFive
      stdout.println(`25 == 25: ${isEqualBase}`)

      // === NOT EQUAL ===
      notEqual <- age1 <> age2
      stdout.println(`25 <> 30: ${notEqual}`)

      ninetyNine <- 99
      notEqualBase <- age1 <> ninetyNine
      stdout.println(`25 <> 99: ${notEqualBase}`)

      // === ORDERING: CT < CT and CT < BaseType ===
      isLess <- age1 < age2
      stdout.println(`25 < 30: ${isLess}`)

      fifty <- 50
      isLessBase <- age1 < fifty
      stdout.println(`25 < 50: ${isLessBase}`)

      isGreater <- age2 > age1
      stdout.println(`30 > 25: ${isGreater}`)

      twenty <- 20
      isGreaterBase <- age2 > twenty
      stdout.println(`30 > 20: ${isGreaterBase}`)

      // === SPACESHIP OPERATOR ===
      cmpResult <- age1 <=> age2
      stdout.println(`25 <=> 30: ${cmpResult}`)

      // === STRING-CONSTRAINED: contains and matches ===
      name1 <- Name("Alice")
      name2 <- Name("Bob")

      same <- name1 == name2
      stdout.println(`Alice == Bob: ${same}`)

      sameBase <- name1 == "Alice"
      stdout.println(`Alice == 'Alice': ${sameBase}`)

      matched <- name1 matches /^A/
      stdout.println(`Alice matches ^A: ${matched}`)

Common mistakes

E50060 — DrivingAge has no equals() method in EK9. Use the == operator for equality comparison. See ek9 -h E50060 for details.

Incorrect:

isEqual <- age1.equals(age2)

Correct:

isEqual <- age1 == age2

E50060 — Name has no equals() method in EK9. Use the == operator for equality comparison. See ek9 -h E50060 for details.

Incorrect:

sameBase <- name1.equals("Alice")

Correct:

sameBase <- name1 == "Alice"
Other ways to ask this
  • Can I compare a constrained type with its base type value?
  • Why does DrivingAge < Integer work?
  • What comparison operators does a constrained type inherit?

Coming from another language?

Java: no constrained types, comparisons use equals()/compareTo() methods. Python: no type constraints, comparison via __eq__/__lt__ etc. Rust: newtype pattern requires implementing PartialEq/PartialOrd traits manually. Go: type aliases inherit comparison operators but no value constraints. Kotlin: value classes require manual operator definitions. EK9: constrained types automatically inherit all comparison operators from the base type with dual signatures (CT==CT and CT==BaseType), enabling natural comparisons with both constrained values and raw literals.

Keywords: dual, inherit, equals, constrained, operator, comparison, matches, signature, contains