Can records extend other records in EK9?
← Classes and OOP · Ref: Q1297
Yes. Records in EK9 follow the same closed-by-default + 'as open' opt-in rule as classes. Mark a base record 'as open' to allow another record to extend it. The child record uses 'extends', calls 'super(...)' in its constructor, and overrides operators with the mandatory 'override' keyword.
THE AS OPEN OPT-IN
Without 'as open' on the base, attempting to extend triggers E05030 (not open to be extended) — same rule as classes. Records are NOT open just because they are records; they are closed by default like every other genus (see Q101).
CHILD-OF-RECORD STRUCTURE
A child record:
- declares 'extends ParentRecord' (or 'is ParentRecord')
- calls 'super(arg, ...)' from its constructor to initialise the inherited fields
- overrides parent operators with 'override operator <symbol> as pure' (override is mandatory; see Q102, Q570)
- can call 'super.<symbol>(...)' inside an override to compose parent behaviour with child fields (e.g. super.$())
- typically declares 'default operator ?' so its own fields gain set/unset semantics
FIELD INHERITANCE — PUBLIC AND VISIBLE
Record fields are public (Q900, Q1060). When a child record extends a parent record, the parent's public fields remain public on instances of the child. Direct access works the same way:
child.parentField OK — public, inherited child.childField OK — public, declared
SAMENESS IS DECLARED ONCE PER HIERARCHY
The comparator <=> (or ==) and its #? are the type's sameness, and a hierarchy declares them exactly once: a child that inherits sameness cannot redeclare <=>, == or #? (E07182/E07184), and ==, <>, <, <=, >, >= are always generated from <=>. So when the child's own fields must take part in comparison, put the sameness on the CHILD (the base declares none) and compare the inherited fields first:
operator <=> as pure -> other as ChildRecord <- rtn as Integer: parentField <=> other.parentField if rtn == 0 rtn: childField1 <=> other.childField1
operator #? as pure <- rtn as Integer: #?parentField xor #?childField1
If instead the base declares the sameness, every child inherits it unchanged; a finer ordering is then a comparator function given to sort.
OPERATOR OVERRIDE CHAINING WITH super.<op>(...)
Other operators, such as $, are overridden and can compose the parent's result. For $ the convention is to append the child's representation:
override operator $ as pure <- rtn as String: `${super.$()} ChildRecord(${childField1}, ${childField2})`
MUTABILITY IS PRESERVED ACROSS THE HIERARCHY
Like all EK9 records, inherited fields are mutable. Mutability is gated by 'as pure' on the calling function/method, not on the type, so the parent-child pair preserves the Liskov Substitution Principle (Q97).
WHY OPERATOR OVERRIDE BUT NOT METHOD OVERRIDE?
Records cannot have methods at all — only constructors and operators (Q97, E07290). So inheritance on records is about extending fields and operators, never adding behaviour as methods. If a chain needs behaviour, switch the construct to a class.
See Q97 for class-vs-record overview. See Q101 for closed-by-default rationale. See Q102 for 'as open' on all genera. See Q570 for override mechanics. See Q900 for the public/private field rule. See Q876 for record basics. See Q98 for record operators. See Q116 for default operator. See Q241 for mutation operators.
Example
defines module qa.classesandoop.recordinheritance defines record <?- Base record marked 'as open' so SimpleRecord may extend it. Without 'as open' the compiler rejects the extension (E05030). -?> BaseRecord as open anything <- 90 BaseRecord() as pure -> anything as Integer this.anything :=: anything //No sameness here: SimpleRecord declares it (sameness is declared once per hierarchy) default operator ? default operator $ <?- Child record extending BaseRecord. - 'extends BaseRecord' inherits the public 'anything' field. - 'super(anyValue)' initialises the inherited field via the parent constructor. - Sameness ('<=>' + '#?') is declared ONCE per hierarchy; the base declares none, so this child declares it over the inherited AND its own fields. - 'override operator $' is mandatory (E05120 otherwise) and 'super.$()' composes the parent's string with the child's fields. - 'default operator ?' synthesises set/unset semantics across all fields. -?> SimpleRecord extends BaseRecord field1 <- 0 count as Integer? check as Integer: 1 SimpleRecord() as pure count :=? 0 SimpleRecord() as pure -> anyValue as Integer field1 as Integer count as Integer check as Integer super(anyValue) this.field1 :=: field1 //'count' is declared uninitialised ('count as Integer?'), so first assignment in a pure constructor uses the guarded ':=?' (':=' reassignment is disallowed in 'pure'; ':=:' deep-copy requires an already-initialised target). this.count :=? count this.check :=: check operator <=> as pure -> other as SimpleRecord <- rtn as Integer: anything <=> other.anything if rtn == 0 rtn: field1 <=> other.field1 if rtn == 0 rtn: count <=> other.count if rtn == 0 rtn: check <=> other.check default operator ? override operator $ as pure <- rtn as String: `${super.$()} SimpleRecord(${field1}, ${count}, ${check})` operator #? as pure <- rtn as Integer: #?anything xor #?field1 xor #?count xor #?check defines program RecordInheritanceDemo() stdout <- Stdout() // 4-argument constructor needs named args (E11062 otherwise) r1 <- SimpleRecord(anyValue: 10, field1: 1, count: 2, check: 3) // Inherited public field 'anything' is directly accessible stdout.println(`r1.anything: ${r1.anything}`) stdout.println(`r1.field1: ${r1.field1}`) stdout.println(`r1.check: ${r1.check}`) // The overridden $ operator composes parent + child output stdout.println(`r1: ${r1}`) // The child's <=> compares the inherited field, then its own fields r2 <- SimpleRecord(anyValue: 10, field1: 1, count: 2, check: 3) r3 <- SimpleRecord(anyValue: 10, field1: 1, count: 2, check: 4) stdout.println(`r1 <=> r2: ${r1 <=> r2}`) stdout.println(`r1 <=> r3: ${r1 <=> r3}`) // Inherited field is mutable — mutability is gated by 'as pure' on the // calling function, not on the type. Inside this (non-pure) program we // may freely modify any inherited or declared field. r1.anything: 999 stdout.println(`After mutation r1.anything: ${r1.anything}`)
Common mistakes
E05030 — Without 'as open' on the base, SimpleRecord cannot extend BaseRecord. EK9 records are closed-by-default just like classes. The fix is to add 'as open' to the base record. See ek9 -h E05030 for details.
Incorrect:
BaseRecord
Correct:
BaseRecord as open
E05120 — When a child record re-declares an operator that exists on the parent, the 'override' keyword is mandatory. Omitting it triggers E05120. This applies to $, ?, and any other operator inherited from the parent - but never to the sameness ('<=>', '==', '#?'), which a hierarchy declares only once (E07182/E07184). See ek9 -h E05120 for details.
Incorrect:
operator $ as pure
Correct:
override operator $ as pure
E50010 — The type named after 'extends' must be a record that exists in scope; a misspelt or undeclared name (and a class cannot stand in for a record, because classes extend classes and records extend records) cannot be found. See ek9 -h E50010 for details.
Incorrect:
SimpleRecord extends BaseRecords
Correct:
SimpleRecord extends BaseRecord
E07290 — Records cannot have methods — only constructors and operators. Even a child record cannot add a method that didn't exist on the parent. If you need behaviour, switch the chain to classes. See ek9 -h E07290 for details.
Incorrect:
this.check :=: check describe() <- rtn as String: "simple" operator <=> as pure
Correct:
this.check :=: check operator <=> as pure
Other ways to ask this
- How do I inherit one record from another in EK9?
- Does EK9 support record inheritance?
- How does 'as open' apply to records?
- How do I override operators on a child record and call the parent's operator?
- What does super.$() mean on a record operator?
Coming from another language?
Java: records (Java 16+) are implicitly final — record-to-record inheritance is impossible. Kotlin: data classes cannot be open/inherited; the language explicitly disallows it. Rust: structs cannot inherit at all (no inheritance, period). Go: struct embedding is composition, not inheritance. Swift: structs are value types and cannot be subclassed. EK9 is unusual in allowing record-to-record inheritance via the same 'as open' opt-in mechanism as classes. This deliberately keeps records as data carriers while permitting layered data hierarchies — useful for event-sourcing, DTO families, and value-type taxonomies where a base shape is extended with extra fields and operator refinements. The override-mandatory rule (E05120) makes the inheritance explicit and prevents accidental shadowing.
Keywords: comparator, record, $, extends, inheritance, super, <=>, operator, BaseRecord, subclass, override, subrecord, #?, as open, data class hierarchy, value type hierarchy, open