How do I constrain generic type parameters in EK9?

← Generics · Ref: Q195

EK9 uses 'constrain by' to restrict generic type parameters to subtypes of a specific class. This enables safe access to methods of the constraining type.

CONSTRAIN BY SYNTAX

Restrict T to subtypes of a base type:

  Handler of type T constrain by Animal

Now T is guaranteed to have all Animal methods.

ACCESSING CONSTRAINED METHODS

Inside the generic, T's methods from Animal are accessible:

  process()
    -> item as T
    name <- item.name()

Because T must be an Animal, calling name() is safe.

COMPILE-TIME SAFETY

Using an incompatible type is a compile error:

  Handler of String  // ERROR: String is not an Animal

Only Animal subtypes are accepted.

FUNCTION CONSTRAINTS

Generic functions also support constraints:

  describe() of type T constrain by Animal
    -> item as T
    <- rtn as String: item.name()

See Q194 for generic classes. See Q58 for generic functions. See Q103 for abstract classes. See Q257 for constrained types (constrain as vs constrain by).

Example

defines module qa.generics.constraints

  defines class

    Animal as abstract
      animalName as String?

      default private Animal() as pure

      Animal() as pure
        -> n as String
        animalName :=? String(n)

      name() as pure
        <- rtn as String: String(animalName)

      override operator ? as pure
        <- rtn as Boolean: animalName?

    Dog is Animal

      Dog() as pure
        -> n as String
        super(n)

      Dog() as pure
        super("Rex")

      bark() as pure
        <- rtn as String: "Woof!"

    Cat is Animal

      Cat() as pure
        -> n as String
        super(n)

      Cat() as pure
        super("Whiskers")

      purr() as pure
        <- rtn as String: "Purr..."

  defines function

    // Generic function constrained to Animal subtypes
    describeAnimal() of type T constrain by Animal as open
      -> item as T
      <- rtn as String: item.name()

  defines program

    GenericConstraintsDemo()
      stdout <- Stdout()

      // === CONSTRAINED GENERIC FUNCTION ===

      dog <- Dog()
      dogDesc <- describeAnimal(dog)
      stdout.println(`Dog: ${dogDesc}`)

      theCat <- Cat()
      catDesc <- describeAnimal(theCat)
      stdout.println(`Cat: ${catDesc}`)

      // The following would NOT compile:
      // describeAnimal("hello")  // String is not an Animal

      stdout.println("Type constraints ensure only Animal subtypes are accepted")

Common mistakes

E50060 — The method 'describe()' does not exist on Dog. Use the generic function 'describeAnimal()' which works on any Animal subtype. See ek9 -h E50060 for details.

Incorrect:

dogDesc <- dog.describe()

Correct:

dogDesc <- describeAnimal(dog)
Other ways to ask this
  • How do I restrict what types can be used with a generic in EK9?
  • What is 'constrain by' in EK9 generics?
  • How do I add type bounds to generics in EK9?

Coming from another language?

Java: <T extends Animal> for upper bounds. C#: where T : Animal. Rust: <T: Animal> trait bounds. Go: type T Animal constraint (Go 1.18+). Kotlin: <T : Animal> bounds. EK9: 'of type T constrain by Animal' reads as natural English, same syntax for functions and classes.

Keywords: parameterized, extends, type-parameter, subtype, constrain, restrict, type, safety, generic, bound, constraint, constant