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 == twentyFive   //DrivingAge == Integer -> Boolean

This means you can compare constrained values directly with values of the base type. A bare numeric literal such as 25 in a comparison is rejected by E11064 (give the literal a meaningful name first, as twentyFive <- 25 does); that rule is about numeric literals, so a String-constrained type can still be compared with a literal such as "Alice".

WHY DUAL SIGNATURES MATTER

Without dual signatures, you would need to wrap every value, as in 'age1 == DrivingAge(25)' (works but verbose), whereas 'age1 == twentyFive' with a named Integer is 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.*/       //String has matches

Note that matches tests the WHOLE string against the pattern, so /^A/ alone would be false for Alice while /^A.*/ is true.
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

#!ek9
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 base-type values (numeric literals still need a name, E11064).

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