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:

  defines function
    describe() of type T constrain by Animal as open
      -> 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

#!ek9
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: bound, constant, constrain, constraint, extends, generic, parameterized, restrict, safety, subtype, type, type-parameter