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:
- records - including abstract records with single inheritance
- functions - specifically abstract functions with single inheritance.
- classes - traditionally focused on base/abstract classes with single inheritance
- traits - single inheritance and multiple inheritance
- components - supporting abstract bases and injection with single 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.
- Traits. EK9 supports multiple inheritance of traits, to help you build Façades and to let one class use both inheritance and automatic delegation to other classes. Traits are implicitly abstract, so they can always be extended. Don't use multiple trait inheritance to build a Façade with too many methods (and too much coupling); a component can compose several implementations, hide their details and expose only what is needed.
- Functions. A function can be treated as an Object and can be abstract, so functions can be used as function delegates and interchanged in stream pipelines, where they can be piped and called (asynchronously if required). They stay type safe: the abstract function is what lets you vary the functions used while the compiler still checks their types. Functions and dynamic functions are similar to lambdas, but they are not, and are not designed to be, true lambdas.
- Components. A component can have an abstract base, and only components can be injected (see dependency injection). Injecting a class or a record is error E08160.
Which constructs you use depends on your background:
- Object-oriented: traits and classes will feel familiar.
- Spring: you will see where components fit.
- Functional: functions/records (and abstract functions) with stream pipelines will be your main focus.
- C: program, function and record may feel most natural to start with.
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 Full source: inheritance_userlist.ek9 - Download inheritance_userlist.ek9 |
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
Download inheritance_closed_list.ek9
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 ?
Download inheritance_composed_list.ek9
Benefits:
- Clear separation: MyList is NOT a List, it USES a List
- Flexible: you can change to a Dict, a Set or your own storage without changing the API
- Encapsulated: the List inside is hidden from consumers
- No inheritance confusion: method resolution is straightforward
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!")
Download inheritance_closed_animal.ek9
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()
Download inheritance_open_animal.ek9
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:
- ✓ Framework base classes (PluginBase, ServiceBase)
- ✓ Designed extension points (Animal, Shape, Vehicle - classic polymorphism)
- ✓ Classes with documented inheritance contracts
- ❌ Concrete business logic (UserService, PaymentProcessor - use traits/components instead)
- ❌ Data classes (User, Order, Product - use traits for polymorphism)
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())
Download inheritance_speakable.ek9
Why traits are better:
- Multiple traits (unlike single class inheritance)
- No fragile base class problem
- Clear contracts for behaviour
- Composition-friendly
Getting Started with Closed-by-Default
What to expect:
- You will try to extend List or Dict and get E05030. Read the error message and use composition instead; every EK9 developer hits this.
- You start asking "does this need inheritance, or can I use composition?" Usually composition is clearer.
-
When you do use inheritance (with
as open), it is intentional and documented. There are no accidental inheritance hierarchies. - Your code becomes more flexible: with fewer subclasses, changing an implementation breaks less.
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
- Traits - EK9's preferred polymorphism mechanism
- Extension by Composition - Using HAS-A instead of IS-A
- E05030: Not open to be extended - Complete error documentation
- Code Quality - How EK9 enforces design principles at compile time
- Design note: why EK9 types are closed by default
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)}`)
Download inheritance_math_operations.ek9
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:
- EK9 infers the list's element type as the common super-function, mathOperation
- The functions are referenced when they are added to the list, not called
- The type of the loop variable op is inferred as mathOperation
- The loop variable op is a fresh variable on each pass: assigning to it inside the loop does not change the list or the loop
- Within the loop op is called with the parameters to produce a result
- String interpolation `` is used, and ${ } converts the returned Float to a String
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.