How do constrained types relate to the type hierarchy?

← Advanced Type System · Ref: Q722

Constrained types are deliberately disconnected from the type hierarchy. They share operators with their base type but are NOT subtypes.

LIKE-A NOT IS-A

DrivingAge shares Integer's operators but is not an Integer:

  age <- DrivingAge(25)
  //intVar as Integer: age    //compile error: INCOMPATIBLE_TYPES
  intVar <- #^ age             //correct: use promote to cross boundary

This is a LIKE-A relationship: DrivingAge behaves like an Integer but is a separate type.

SUPER IS ANY

In the type hierarchy, constrained types have Any as their super type, not the base type:

  DrivingAge -> Any      (not DrivingAge -> Integer -> Any)

This prevents polymorphic substitution that would bypass validation.

CANNOT CONSTRAIN A CONSTRAINED TYPE

Constraining is a deliberately SIMPLE, one-level concept: a base type limited to a restricted range of values. You cannot constrain (or alias) an already-constrained type — once constrained it is its own distinct type and is no longer a candidate to be constrained. The compiler raises TYPE_CANNOT_BE_CONSTRAINED (E04010):

  Index as Integer constrain as > 0
  //DBIndex as Index constrain as < 1000000   //compile error E04010: 'Index' is not a candidate to be constrained

To make a more specific type, constrain the BASE type directly with the combined constraint:

  BoundedIndex as Integer constrain as > 0 and < 1000000

CANNOT EXTEND VIA INHERITANCE

Constrained types are closed — you cannot extend them with 'extends' or 'as open', nor constrain/alias them further. The only way to create a more specific type is to declare a new constrained type over the BASE type with the combined constraint.

EXPLICIT CONVERSION

To cross type boundaries, use constructors or promote:

  age <- DrivingAge(25)
  rawInt <- #^ age              //promote to Integer
  newAge <- DrivingAge(rawInt)  //construct from Integer

See Q257 for constrained type overview. See Q718 for promote operator. See Q101 for closed-by-default types.

Example

defines module qa.advancedtypes.constrainedhierarchy

  defines type

    // === BASE CONSTRAINED TYPE ===
    Index as Integer constrain as
      > 0

    // === A MORE SPECIFIC TYPE: constrain the BASE type directly (NOT 'as Index') ===
    //You cannot constrain an already-constrained type — combine the constraints over the base type.
    BoundedIndex as Integer constrain as
      > 0 and < 1000000

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

  defines function

    <?-
      Takes raw Integernot DrivingAge.
    -?>
    processRawInteger() as pure
      -> intValue as Integer
      <- result as String: `Integer value: ${intValue}`

  defines program

    ConstrainedHierarchyDemo()
      stdout <- Stdout()

      // === LIKE-A NOT IS-A ===
      age <- DrivingAge(25)
      stdout.println(`DrivingAge: ${age}`)

      //DrivingAge is NOT an Integer — must promote
      rawInt <- #^ age
      output <- processRawInteger(rawInt)
      stdout.println(output)

      // === CANNOT CONSTRAIN A CONSTRAINED TYPE: constrain the base directly ===
      idx <- Index(42)
      stdout.println(`Index: ${idx}`)

      bIdx <- BoundedIndex(500)
      stdout.println(`BoundedIndex: ${bIdx}`)

      //Out-of-range via the fallible factory (never panics — returns an unset value)
      badIdx <- BoundedIndex().of(0)
      stdout.println(`BoundedIndex(0) valid: ${badIdx?}`)

      // === PROMOTE WORKS ON ALL CONSTRAINED TYPES ===
      rawFromIndex <- #^ idx
      stdout.println(`Index promoted to Integer: ${rawFromIndex}`)

      // === CONSTRUCTION CROSSES BOUNDARY ===
      newAge <- DrivingAge(30)
      rawAge <- #^ newAge
      reconstructed <- DrivingAge(rawAge)
      stdout.println(`Round-trip: ${reconstructed}`)

      // === LIST OF CONSTRAINED TYPE WORKS ===
      ages <- List() of DrivingAge
      ages += DrivingAge(25)
      ages += DrivingAge(30)
      ages += DrivingAge(40)
      stdout.println(`Ages count: ${length ages}`)

Common mistakes

E04010 — You cannot constrain an already-constrained type (Index is itself constrained). Constraining is one-level — constrain the base type directly with the combined constraint. See ek9 -h E04010 for details.

Incorrect:

DBIndex as Index constrain as < 1000000

Correct:

BoundedIndex as Integer constrain as
      > 0 and < 1000000

E50060 — Index has no intValue() method in EK9. Use the promote operator (#^) to extract the base type value. See ek9 -h E50060 for details.

Incorrect:

rawFromIndex <- idx.intValue()

Correct:

rawFromIndex <- #^ idx
Other ways to ask this
  • Why is a constrained type not a subtype of its base type?
  • What does LIKE-A mean for constrained types?
  • Can I constrain a constrained type?

Coming from another language?

Java: wrapper classes are subtypes of Object, custom value wrappers can be subtypes of base class. Python: no type hierarchy constraints, duck typing. Rust: newtype pattern creates a separate type (like EK9), explicit From/Into for conversions. Go: type definitions create new types disconnected from base (similar to EK9). Kotlin: value classes are still subtypes of their underlying type. Swift: no newtype pattern, typealias shares identity. EK9: constrained types are LIKE-A (share operators) not IS-A (not subtypes) — super is Any, fully disconnected, and constraining is strictly one-level: you constrain a base type, never another constrained type.

Keywords: alias, hierarchy, explicit, E04010, constrained, boundary, extend, like-a, subtype, disconnected, any