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
- 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

OPERATOR OVERRIDE CHAINING WITH super.<op>(...)
The canonical pattern for the comparator <=> on a child record is: compute the parent's result first, then refine with the child's own fields. The same pattern applies to $ (string) and #? (hashcode):

  override operator <=> as pure
    -> other as ChildRecord
    <- rtn as Integer: super.<=>(other)
    if rtn == 0
      rtn: childField1 <=> other.childField1
    if rtn == 0
      rtn: childField2 <=> other.childField2

For $ the convention is to append the child's representation:

  override operator $ as pure
    <- rtn as String: `${super.$()} ChildRecord(${childField1}, ${childField2})`

For #? the convention is to xor with the child's hashes:

  override operator #? as pure
    <- rtn as Integer: super.#?() xor #?childField1 xor #?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

      default operator <=>

      default operator ?

      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.
      - 'override operator <=>' is mandatory (E05120 otherwise).
      - 'super.<=>(other)' composes the parent comparison 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

      override operator <=> as pure
        -> other as SimpleRecord
        <- rtn as Integer: super.<=>(other)

        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})`

      override operator #? as pure
        <- rtn as Integer: super.#?() 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 overridden <=> operator chains super.<=> with child 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 operator inherited from the parent. See ek9 -h E05120 for details.

Incorrect:

operator <=> as pure

Correct:

override operator <=> as pure

E50010 — A class cannot extend a record because they are different construct genera. Classes extend classes; records extend records. The compiler rejects cross-genus inheritance. See ek9 -h E50010 for details.

Incorrect:

SimpleClass extends BaseRecord

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:

describe() <- rtn as String: ...

Correct:

default operator
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.<=>(other) 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: BaseRecord, operator, <=>, super, inheritance, extends, $, record, comparator, open, value type hierarchy, data class hierarchy, as open, #?, subrecord, override, subclass