Inheritance in EK9
There have been several examples of inheritance of various constructs in several of the other sections. See the list of examples for details.
This section explains the use inheritance and also why other mechanisms are sometimes recommended. The concept of inheritance is an Object-Oriented one; EK9 uses this same concept but with constructs other than just classes. It is the valuable concept of polymorphism that is the vital aspect we are looking to employ; irrespective of the construct.
Constructs that support inheritance
EK9 has a number of constructs that are suitable for particular tasks, many of these 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
Rationale
EK9 does have a wide range of constructs; whereas many languages focus on classes or maybe just functions as the primary structural construct. Each of the constructs in EK9 is designed to provide functionality in a specific role. This means that capabilities such as inheritance/composition are used in a very specific manner and for some constructs must be limited (or enhanced).
While it is true to say the mechanisms of abstraction, encapsulation and inheritance are important; polymorphism is really is the main power behind Object-Oriented Programming. EK9 applies polymorphism to several constructs in addition to classes; this widens its use.
EK9 supports multiple inheritance of traits. This is to facilitate the development of Façades and enable
a single class to support both inheritance and automatic delegation to other classes.
Traits are all implicitly abstract and can therefore be extended. Take care here not to
use multiple inheritance of traits to build a Façade with too many methods (and coupling).
You may find that using components provides a suitable alternative and enables you to compose
various implementations and hide the implementation details but expose just enough via the component.
Functions can also treated as Objects and can be abstract; this enables them to be used as function delegates; hence they may be used in an interchangeable manner in stream pipelines. They can actually be piped through pipelines and called (asynchronously if required). But importantly they are type safe and can be type checked. Only by having the concept of an abstract function could the actual types of functions being used, altered and varied in a type safe manner.
Functions and dynamic functions are not lambdas - they are similar - but they are not (and are not designed to be) true lambdas.
Classes, records and components only support single inheritance, but classes can also exhibit multiple traits.
Denoting components as a specific construct that support an abstract base, enables components can be injected (see dependency injection). This is not the case with classes or records they cannot be 'injected'.
Different constraints and capabilities on the constructs with respect to inheritance enable each construct to fulfil the role it is designed for, rather than attempting to force the class to fit every role.
Those with and Object-Oriented background will probably feel at home with traits and classes. Those that have used Spring will probably see where components can fit in. Developers with a more functional background will look to functions/records (and abstract functions) with stream pipelines as their main focus.
It is not necessary to use every type of construct in a solution; only use the features and capabilities you need or feel most at home with. For example C programmers may find just using program, function and record feels most natural to start with.
With EK9, there are a number of tools (constructs) that are available; adopting a pure Functional, total Object-Oriented or a hybrid approach to development is your choice. For different projects you will probably alter the balance of the range of constructs used.
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, it's the opposite - types are closed by default. You must explicitly
opt into extensibility with the as open modifier.
This is a fundamental paradigm shift that prevents entire categories of bugs and design problems.
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
1. Semantic Integrity
Extending concrete types like List or Dict mixes collection semantics with application logic, creating 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 // 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
|
Why composition wins: Clear separation of concerns, flexible implementation, no inheritance confusion
2. Modern Language Precedent
EK9 follows modern language design that has learned from Java's mistakes:
- Kotlin: Classes are
finalby default - must addopento allow inheritance - Swift: Structs are closed (cannot inherit) - only classes can inherit, and must be marked
open - Rust: No class inheritance at all - composition and traits only
- Go: No class inheritance - struct composition and interfaces
Industry Consensus: The 2010s saw a clear shift away from Java's "everything is open" model. Languages designed after 2010 almost universally default to closed types.
3. Java's Historical Design Error
Java's decision to make everything extendable by default is now widely recognized as a mistake:
| Problem | Example | Impact |
|---|---|---|
| Fragile Base Class Problem | Extending ArrayList, overriding one method breaks others | Hard-to-diagnose bugs, framework lock-in |
| Semantic Confusion | Stack extends Vector (Java standard library) |
Stack IS-A Vector? Violates Liskov Substitution Principle |
| Breaking Encapsulation | Subclasses access internal implementation details | Cannot refactor base class without breaking subclasses |
Joshua Bloch (Effective Java): "Design and document for inheritance or else prohibit it." EK9 takes the second approach - prohibit by default, allow when explicitly designed for it.
What You Can't Do (And What to Do Instead)
Cannot Extend Built-in Concrete Types
#!ek9
defines class
MyList extends List of String // ❌ Compile error: E05030
// List is not open to be extended
additionalMethod()
//...
Error: E05030: 'List' is not open to be extended
Why blocked: List, Dict, Set, String, Integer, etc. are sealed - they have specific semantics that shouldn't be mixed with application logic.
Use Composition Instead
#!ek9
defines class
MyList
items as List of String // ✓ Composition - HAS-A relationship
add(item as String)
if isValid(item)
items += item
additionalMethod()
//...
Benefits:
- Clear separation: MyList is NOT a List, it USES a List
- Flexible: Can change to Dict, Set, or custom storage without affecting API
- Encapsulated: Internal List implementation hidden from consumers
- No inheritance confusion: Method resolution is straightforward
User Classes Closed by Default
#!ek9
defines class
Animal
name as String
speak()
stdout.println("Generic animal sound")
Dog extends Animal // ❌ Compile error: E05030
// Animal is not open to be extended
To allow extension, mark as open:
#!ek9
defines class
Animal as open // ✓ Explicitly opened for extension
name as String
speak()
stdout.println("Generic animal sound")
Dog extends Animal // ✓ Now works - Animal is open
speak()
stdout.println("Woof!")
When to Use "as open"
Use as open when you've 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
Instead of class inheritance, EK9 encourages traits for polymorphic behavior:
#!ek9
defines trait
Speakable // Open for implementation - no "as open" needed
speak()
defines class
Dog with trait of Speakable // ✓ Implements trait
name as String
speak()
stdout.println("Woof!")
Cat with trait of Speakable // ✓ Implements trait
name as String
speak()
stdout.println("Meow!")
defines function
makeSpeak()
-> animal as Speakable // Polymorphism via trait
animal.speak()
Why traits are better:
- Multiple traits (unlike single class inheritance)
- No fragile base class problem
- Clear contracts for behavior
- Composition-friendly
Getting Started with Closed-by-Default
Your First Week
- Day 1-2: You'll try to extend List/Dict and get E05030 errors. Read the error message. Use composition instead. This is normal - every EK9 developer hits this!
- Day 3-5: You start thinking "does this need inheritance, or can I use composition?" Most of the time, composition is clearer.
- Week 2: You appreciate that when you DO use inheritance (with
as open), it's intentional and documented. No accidental inheritance hierarchies. - Month 1: You realize your code is more flexible. Changing implementation doesn't break subclasses because there are fewer subclasses - you use traits and composition instead.
Key Principle: Favor composition over inheritance. Use traits for polymorphism.
Only use class inheritance (as open) when you've explicitly designed a class
hierarchy with documented extension contracts.
Remember: Closed-by-default protects you from the fragile base class problem and semantic confusion. When you want polymorphism, ask: "Do I need IS-A (inheritance) or CAN-DO (trait)?" Most of the time, traits are the answer.
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
Extending Functionality
Only if a construct is open or abstract can it be extended but see traits as these can provide more fine-grained control.
Overriding Methods
When looking to alter a methods functionality in classes, traits or components; the keyword override must be used. This is covered in more detail in the methods section. Actually the same applies to operators on records, classes and traits. If you are familiar with Kotlin/Java and have used the 'Annotations' such as @Override then this concept will be familiar to you.
Additional Examples
Records
The record example shows additional properties being added to records, but also uses a wide range of operators including overriding operators.
Functions
Below is an additional example of abstract functions; this highlights how functions can be treated in an Object like manner. Also note this approach is SOLID even though we are dealing with functions and not classes.
#!ek9
defines module introduction
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)}`)
//EOF
The above example demonstrates how the arithmetic functions can be defined to meet the same signature (mathOperation); and can be treated in a polymorphic manner. The loop in the 'MathExample' program can call each of the functions without having to be specific as to which function they are. But critically this is type safe and exhibits polymorphism.
There are a couple of interesting points to note in this example above:
- EK9 detects the super-type of the functions when the list is defined as mathOperation
- The functions are referenced when being added to the list, they are not called
- The type for the loop variable op is inferred as mathOperation
- The loop variable op is actually immutable - it cannot be altered by the developer manually
- Within the loop the op is called with the parameters to produce a result
- String interpolation `` has been used and ${ } converts the returning Float to a String
One could imagine this type of dynamic capability being used in a wide range of applications that triggers varying functionality in specific circumstances (UI button clicks, for example).
Classes
This class example shows several classes being extended, some are open for further extension. This second class example shows classes with specific traits. This final example shows how class inheritance can actually be avoided through the use of composition.
Traits
This example shows traits being used for multiple inheritance and also controlling/limiting how classes can inherit from bases.
Summary
Inheritance is useful in many contexts; EK9 has attempted to limit inheritance in some scenarios and enhance it in others. It has also provided an alternative approach to extending and reusing functionality through composition.
Next Steps
Back to functions, specifically Dynamic Functions; these follow the same thread of inheritance (but inheritance from abstract functions delivering polymorphism).