Why can't I extend a class in EK9?
← Classes and OOP · Ref: Q904
EK9 types are CLOSED by default — like Kotlin's classes which are final unless marked 'open'. You must explicitly opt into extensibility with 'as open'.
CLOSED BY DEFAULT
All classes, records, and built-in types cannot be extended unless marked:
defines class Animal closed — cannot extend name <- String()
Dog extends Animal ERROR E05030 — Animal is not open
OPT INTO EXTENSION
Use 'as open' to allow extension:
defines class Animal as open now extensible name <- String()
Dog extends Animal OK — Animal is open
BUILT-IN TYPES CANNOT BE EXTENDED
List, Dict, Optional, Result, and all built-in concrete types are CLOSED:
MyList extends List of String ERROR — List is closed
Use composition/delegation instead:
defines class ValidatedList items as List of String: List() of String addValidated() -> item as String if length item > 0 items += item
WHY CLOSED BY DEFAULT
- Like Kotlin's final by default — prevents fragile base class problem
- Forces explicit design for inheritance
- Encourages composition over inheritance (more flexible)
- Simplifies generated code (no virtual dispatch needed)
- Modern consensus: Java's open-by-default was a design mistake
RECORDS ARE ALWAYS CLOSED
Records cannot be marked 'as open' — they are pure value types:
defines record Point x as Float: 0.0 y as Float: 0.0
See Q93 for class basics. See Q97 for records vs classes.
Example
defines module qa.classes.closedtypes defines class <?- CLOSED class — cannot be extended. Like Kotlin's final (default behavior). -?> DatabaseConnection connectionUrl as String: String() DatabaseConnection() -> connectionUrl as String this.connectionUrl :=: connectionUrl getUrl() as pure <- rtn as String: connectionUrl default operator <?- OPEN class — explicitly allows extension. Like Kotlin's 'open class'. -?> Shape as abstract shapeName as String: String() Shape() -> shapeName as String this.shapeName :=: shapeName area() as pure abstract <- rtn as Float? getName() as pure <- rtn as String: shapeName default operator <?- Extends an open class — this is allowed. -?> Rectangle extends Shape rectWidth as Float: 0.0 rectHeight as Float: 0.0 Rectangle() -> rectWidth as Float rectHeight as Float super("Rectangle") this.rectWidth :=: rectWidth this.rectHeight :=: rectHeight override area() as pure <- rtn as Float: rectWidth * rectHeight default operator <?- Composition instead of inheritance. Used when the base type is closed. -?> ValidatedItems items as List of String: List() of String addItem() -> item as String if length item > 0 items += item getCount() as pure <- rtn as Integer: length items default operator defines program ClosedTypesDemo() stdout <- Stdout() // Closed class — used directly, not extended conn <- DatabaseConnection("jdbc:postgresql://localhost:5432/mydb") stdout.println(`Connection: ${conn.getUrl()}`) // Open class — can be extended rect <- Rectangle(5.0, 3.0) stdout.println(`Shape: ${rect.getName()}, Area: ${rect.area()}`) // Composition — when you can't extend validItems <- ValidatedItems() validItems.addItem("hello") validItems.addItem("") validItems.addItem("world") stdout.println(`Valid items: ${validItems.getCount()}`)
Other ways to ask this
- What does 'closed by default' mean in EK9?
- How do I make a class extensible in EK9?
- What is the 'as open' modifier in EK9?
- Why does E05030 say 'not open to be extended'?
Coming from another language?
Kotlin: classes are final by default, use 'open' keyword — identical to EK9's approach. Java: classes are open by default — opposite of EK9, considered a historical design mistake. Rust: no inheritance at all — composition via traits only. Swift: classes are not final by default but 'final' keyword available. Go: no inheritance — composition only. C++: classes are extensible by default. EK9: closed by default like Kotlin — use 'as open' to allow extension.
Keywords: final, E05030, Kotlin, open, closed, inherit, composition, extend