Inheritance in EK9

Other sections have shown inheritance of several constructs; see the list of examples.

This section explains how to use inheritance, and why other mechanisms are sometimes better. Inheritance is an object-oriented idea, and EK9 applies it to constructs other than classes. What matters is polymorphism, whatever the construct.

Constructs that support inheritance

Each EK9 construct suits a particular task, and many of them support inheritance:

Classes, records and components support single inheritance only, but a class can also exhibit several traits.

Rationale

Why EK9 does it this way. Many languages make the class (or the function) the one structural construct. EK9 has a range of constructs, each designed for a specific role, so inheritance and composition are limited on some and enhanced on others. Abstraction, encapsulation and inheritance matter, but polymorphism is the real power of object-oriented programming, and EK9 widens its use by applying it to more than classes. The class is not forced to fit every role.

Which constructs you use depends on your background:

You don't need every construct; use the ones you need. A purely functional, fully object-oriented or hybrid approach is your choice, and the balance will change from project to project.

What's Different: Closed by Default

Coming from Java, Python, or C#? You expect classes and types to be open for extension by default. In EK9 types are closed by default: extending one that is not as open is error E05030.

The Rule: Closed Unless Explicitly Opened

Type Default State To Allow Extension
Built-in concrete types
(List, Dict, String, Integer)
Closed - CANNOT extend Not possible - sealed forever
User-defined classes/records Closed by default Must add as open modifier
Abstract classes/records Open for implementation Already open (must be extended)
Traits Open for implementation Already open (contracts for polymorphism)

Why Closed by Default? The Evidence

Extending a concrete type such as List or Dict mixes collection semantics with application logic, which makes fragile, hard-to-understand code:

❌ Java Pattern (Anti-Pattern) ✓ EK9 Pattern (Composition)
class UserList extends ArrayList<User> {
  // Mixes collection logic with domain logic
  void addIfValid(User u) {
    if (u.isValid()) {
      add(u);  // Which add? ArrayList's? Mine?
    }
  }
  // IS-A relationship wrong:
  // UserList IS-A ArrayList? No!
  // UserList HAS-A list of users
}
UserList
  users as List of User: List()  // HAS-A relationship

  addIfValid()
    -> user as User
    if user.isValid()
      users += user
  // Clear separation: UserList uses List
  // Not confused with List itself
  // Can change storage (Set? Array?) easily

  // ... more

Why composition wins: a clear separation of concerns, a flexible implementation, and no doubt about which method runs.

Kotlin, Rust and Go moved away from Java's open-by-default model, Swift closes a class to other modules unless it is marked open, and Joshua Bloch's Effective Java advises "Design and document for inheritance or else prohibit it". EK9 prohibits by default and allows extension where it was designed for. Design note: why EK9 types are closed by default sets out the precedent and the problems open-by-default causes.

What You Can't Do (And What to Do Instead)

Cannot Extend Built-in Concrete Types

Fails with E05030: not open to be extended

#!ek9
defines module bad.site.inheritance.closedlist

  defines class

    MyList extends List of String
      // List is not open to be extended
      additionalMethod() as pure
        <- rtn as Boolean: true

List, Dict, Set, String, Integer and the other built-in types are sealed: their semantics should not be mixed with application logic.

Use Composition Instead

#!ek9
defines module site.inheritance.composedlist

  defines class

    MyList
      items as List of String: List()  // Composition - HAS-A relationship

      add()
        -> item as String
        if item?
          items += item

      additionalMethod() as pure
        <- rtn as Integer: length items

      default operator ?

Benefits:

User Classes Closed by Default

Fails with E05030: not open to be extended

#!ek9
defines module bad.site.inheritance.closedanimal

  defines class

    Animal
      name as String: String()

      speak()
        Stdout().println("Generic animal sound")

      default operator ?

    Dog extends Animal
      // Animal is not open to be extended
      override speak()
        Stdout().println("Woof!")

To allow extension, mark as open:

#!ek9
defines module site.inheritance.openanimal

  defines class

    Animal as open  // Explicitly opened for extension
      name as String: String()

      speak()
        Stdout().println("Generic animal sound")

      default operator ?

    Dog extends Animal  // Now works - Animal is open
      override speak()
        Stdout().println("Woof!")

  defines program

    AnimalSounds()
      for animal in [Animal(), Dog()]
        animal.speak()

A method that replaces the one it inherits says so with override; the program prints:

Generic animal sound
Woof!

When to Use "as open"

Use as open when you have designed the class for inheritance:

Preferred Alternative: Traits for Polymorphism

For polymorphic behaviour, EK9 encourages traits rather than class inheritance:

#!ek9
defines module site.inheritance.speakable

  defines trait

    Speakable  // Open for implementation - no "as open" needed
      speak()

  defines class

    Dog with trait of Speakable  // Implements trait
      name as String: String()

      override speak()
        Stdout().println("Woof!")

      default operator ?

    Cat with trait of Speakable  // Implements trait
      name as String: String()

      override speak()
        Stdout().println("Meow!")

      default operator ?

  defines function

    makeSpeak()
      -> animal as Speakable  // Polymorphism via trait
      animal.speak()

  defines program

    MakeThemSpeak()
      makeSpeak(Dog())
      makeSpeak(Cat())

Why traits are better:

Getting Started with Closed-by-Default

What to expect:

Key Principle: Favour composition over inheritance and use traits for polymorphism. Use class inheritance (as open) only for a class hierarchy you designed, with documented extension contracts.

Closed by default protects you from the fragile base class problem and from semantic confusion. When you want polymorphism, ask: "Do I need IS-A (inheritance) or CAN-DO (trait)?" Usually it is a trait.

See Also

Extending Functionality

Only an open or abstract construct can be extended; traits give you finer control.

Overriding Methods

To change a method's behaviour in a class, trait or component, you must use the keyword override (E05120); methods covers this in more detail. The same applies to operators on records, classes and traits. If you have used @Override in Kotlin or Java, this will be familiar.

Additional Examples

Records

The record example adds properties to records and uses a wide range of operators, including overriding operators.

Functions

This example of abstract functions shows functions treated like objects. The approach is SOLID, even though these are functions and not classes.

#!ek9
defines module site.inheritance.mathoperations
  defines function

    mathOperation() as pure abstract
      ->
        x as Float
        y as Float
      <-
        result as Float?

    add() is mathOperation as pure
      ->
        x as Float
        y as Float
      <-
        result as Float: x + y

    subtract() is mathOperation as pure
      ->
        x as Float
        y as Float
      <-
        result as Float: x - y

    divide() is mathOperation as pure
      ->
        x as Float
        y as Float
      <-
        result as Float: x / y

    multiply() is mathOperation as pure
      ->
        x as Float
        y as Float
      <-
        result as Float: x * y

  defines program

    MathExample()
      stdout <- Stdout()
      stdout.println("Math Operation Example")

      for op in [add, subtract, divide, multiply]
        stdout.println(`Result: ${op(21, 7)}`)

Running the program 'MathExample' prints:

Math Operation Example
Result: 28.0
Result: 14.0
Result: 3.0
Result: 147.0

The arithmetic functions all meet the same signature (mathOperation), so they can be treated polymorphically: the loop in 'MathExample' calls each one without knowing which function it is, and it is type safe. Points to note:

This kind of dynamic capability fits applications that trigger different functionality in specific circumstances, such as UI button clicks.

Classes

This class example shows several classes being extended, some of them open for further extension. This second class example shows classes with specific traits. This final example shows how composition avoids class inheritance.

Traits

This example shows traits used for multiple inheritance, and to control how classes can inherit from them.

Summary

Inheritance is useful in many contexts. EK9 limits it in some places, enhances it in others, and offers composition as another way to extend and reuse functionality.

Next Steps

Back to functions: Dynamic Functions follow the same thread, inheriting from abstract functions to deliver polymorphism.