How do I create a subset of enum values in EK9?

← Enumerations · Ref: Q223

EK9 supports constrained enumerations that create a new type limited to a subset of values from a base enum. This is a compile-time type-safe mechanism, not a runtime collection.

CONSTRAINED SYNTAX

Declare a constrained enum with 'as BaseEnum constrain as' followed by quoted values joined with 'or':

  RedSuit as CardSuit constrain as
    "Hearts" or "Diamonds"

This creates a NEW type RedSuit that can only hold Hearts or Diamonds.

NOT A SUBTYPE

Constrained enums are separate types, not subtypes of the base enum. You cannot assign a RedSuit to a CardSuit variable or vice versa. This is intentional: the type system guarantees that each variable holds exactly the values its type allows.

CONSTRUCTION

A bare constructor ASSERTS validity: RedSuit(CardSuit.Hearts) yields a set value, but a set value outside the subset is invalid - it PANICS at runtime, and a violating literal constant (e.g. RedSuit("Clubs")) is the compile error E08260. To VALIDATE an untrusted base value without panicking, use the fallible factory .of(baseValue), which returns a set value when valid and an UNSET value when it violates the constraint: RedSuit().of(CardSuit.Hearts) is set, RedSuit().of(CardSuit.Clubs) is unset. Because .of(...) takes a base-enum value, parse a String into the base enum first: RedSuit().of(CardSuit("Hearts")) is set, RedSuit().of(CardSuit("Clubs")) is unset. Combine with guard expressions for clean checking: 'if parsed <- RedSuit().of(CardSuit("Diamonds"))'.

ITERATION

Constrained enums support iteration and streaming just like base enums: 'for suit in RedSuit' iterates over the constrained subset.

OPERATORS

Constrained enums inherit all comparison, string conversion, and query operators from the base enum. You can compare constrained values, check with ?, convert to string, and use first/last.

See Q99 for basic enumerations. See Q219 for auto-generated operators. See Q195 for generic constraints.

Example

defines module qa.enums.constrained

  defines type

    CardSuit
      Hearts
      Diamonds
      Clubs
      Spades

    RedSuit as CardSuit constrain as
      "Hearts" or "Diamonds"

    BlackSuit as CardSuit constrain as
      "Clubs" or "Spades"

  defines program

    EnumConstrainedDemo()
      stdout <- Stdout()

      // === CONSTRUCTION FROM BASE ENUM ===
      // Use the fallible factory .of(baseValue) for validation: it returns
      // a SET value when the base enum value satisfies the constraint and an
      // UNSET value when it violates it - never panics. This is the pattern
      // for validating untrusted/boundary values.

      redCard <- RedSuit().of(CardSuit.Hearts)
      stdout.println(`Red suit: ${redCard}, isSet: ${redCard?}`)

      blackCard <- BlackSuit().of(CardSuit.Spades)
      stdout.println(`Black suit: ${blackCard}, isSet: ${blackCard?}`)

      // === CONSTRAINT ENFORCEMENT ===
      // Clubs is not in RedSuit, Hearts is not in BlackSuit: .of(...) returns
      // an UNSET value rather than panicking.

      invalidRed <- RedSuit().of(CardSuit.Clubs)
      stdout.println(`Clubs in RedSuit isSet: ${invalidRed?}`)

      invalidBlack <- BlackSuit().of(CardSuit.Hearts)
      stdout.println(`Hearts in BlackSuit isSet: ${invalidBlack?}`)

      // === OPERATORS WORK ON CONSTRAINED ENUMS ===

      red1 <- RedSuit(CardSuit.Hearts)
      red2 <- RedSuit(CardSuit.Diamonds)
      stdout.println(`Hearts < Diamonds: ${red1 < red2}`)
      sameAsRed1 <- RedSuit(CardSuit.Hearts)
      stdout.println(`Hearts == Hearts: ${red1 == sameAsRed1}`)
      stdout.println(`String: ${red1}`)
      stdout.println(`Hashcode: ${#?red1}`)

      // === STRING VALIDATION VIA THE BASE ENUM AND .of(...) ===
      // The fallible factory .of(...) takes a base-enum value, so parse the
      // (untrusted) String into a CardSuit first, then validate against the
      // constrained subset. A value outside the subset yields an UNSET result.

      fromString <- RedSuit().of(CardSuit("Hearts"))
      stdout.println(`From string: ${fromString}, isSet: ${fromString?}`)

      invalidString <- RedSuit().of(CardSuit("Clubs"))
      stdout.println(`Invalid string isSet: ${invalidString?}`)

      // === GUARD EXPRESSION WITH CONSTRAINED ENUM ===

      if parsed <- RedSuit().of(CardSuit("Diamonds"))
        stdout.println(`Parsed: ${parsed}`)

      // === UNSET CONSTRAINED ENUM ===

      unsetRed <- RedSuit()
      stdout.println(`Unset RedSuit isSet: ${unsetRed?}`)

      // === ITERATION OVER CONSTRAINED ENUM ===

      for suit in RedSuit
        stdout.println(`Red: ${suit}`)

      // === FULL BASE ENUM STILL WORKS ===

      allSuits <- cat CardSuit | collect as List of CardSuit
      stdout.println(`All suits: ${length allSuits}`)

Common mistakes

E50060 — Stdout does not have a display() method. The correct method is println(). Calling a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.

Incorrect:

stdout.display(`Red suit: ${redCard}, isSet: ${redCard?}`)

Correct:

stdout.println(`Red suit: ${redCard}, isSet: ${redCard?}`)
Other ways to ask this
  • What are constrained enumerations in EK9?
  • How do I restrict an enum to a subset of values?
  • Can I create a type-safe enum subset in EK9?

Coming from another language?

Java: EnumSet.of(Hearts, Diamonds) is runtime only, no type safety, any CardSuit can be passed where EnumSet expected. Python: no equivalent, must use runtime validation. Rust: no equivalent, would need separate enum plus TryFrom conversion. Go: no equivalent, no enum type at all. Kotlin: no equivalent, sealed interface can approximate but verbose. C#: no equivalent, FlagsAttribute is bitwise not subset. TypeScript: union types are structural not nominal. EK9: compile-time type-safe enum subsets, constrained type is separate from base type.

Keywords: separate, subset, card, define, constrain, suit, compile, type, enum, enumeration, safe, restricted