E07238: a type that adds fields, concrete or abstract, must declare its own '$', '$$' and ':=:', and its own sameness ('<=>' or '==', with '#?'), where it would inherit one: the inherited operator was written for the super's fields and cannot know these - use 'default operator' for each (or the bare 'default operator') or write one

Category: E07xxx Full resolution · Reported in phase: FULL_RESOLUTION · Classification: INHERITED_OPERATOR_OVER_NEW_FIELDS · All error codes

What went wrong

This type adds fields of its own but would INHERIT '$', '$$', ':=:' or its sameness ('<=>' or '==', with '#?') from a type above it. That operator was written for the super's fields: it cannot know the ones this type adds. So the new fields would be silently missing from the string ('$') and the JSON ('$$'), a copy (':=:') would copy only half of the object, and two values that differ only in the new fields would be the same: '==' true, '<=>' 0, equal '#?'. A Leaf that adds 'name' below a Base with '$' and its own 'id' would print as Base(id=1), with 'name' silently missing.

This is the same rule E07235 applies to '?', extended to the operators whose inherited version would lose data. For SAMENESS it applies to a concrete AND to an abstract type that has per-type sameness: below an ordered super (one with '<=>') it declares its own '<=>', below an equatable-only super (one with '==' and no '<=>') its own '=='. Nothing at run time catches the inherited one - both values are the same run-time type. It does not apply where the sameness was WRITTEN BY HAND on an abstract base or a trait: that is hierarchy-wide sameness (every Shape by area), and a sub-type may not change it (E07182). Fields a sub-type adds below a hierarchy-wide sameness are not compared on purpose: a Card whose trait orders by 'rank()' has Card(5, "hearts") == Card(5, "spades"). One sameness per type; if you want another view (by suit as well, 'aces high'), put it into the trait's ordering or write a comparator function and 'sort by' it (see ek9 -q 2280).

An abstract type that adds fields therefore has two choices, and they differ:

  • 'default operator' (or 'default operator <=>' / '==') is PER-TYPE sameness. It compares this type's fields and the super's, and each sub-type that adds fields declares its own again.
  • A '<=>' or '==' written BY HAND on the abstract type is HIERARCHY-WIDE sameness. It orders every pair of values in the hierarchy, whatever their run-time types, and is sealed: no sub-type may declare its own (E07182), so fields a sub-type adds later are not compared.

It follows the super CLASS chain only. A concrete '$' inherited from a TRAIT is not checked: a trait has no fields, so its '$' is a deliberate representation (often built on an abstract method such as 'label()'), not a rendering of fields it could miss.

A copy (':=:') is polymorphic: each type that declares ':=:' also overrides the inherited ':=:(Base)', so a copy through a Base-typed reference runs the object's own copy. It copies in full between objects of the same class, and throws an Exception naming the classes when the source is a different class (a Middle into a Leaf, or a sub-type that adds fields).

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 116 (What does 'default operator' generate?)
  • ek9 -q 1314 (Why does 'default operator' in a derived class fail when the base class lacks operators?)
  • ek9 -q 2248 (Why were two of my subclass objects equal when only the added field was different?)
  • ek9 -q 2280 (Why is my Card equal to another Card of the same rank but a different suit?)

How to fix it

Declare the operator on the type that adds the fields (good.ek9 in the [EXAMPLE] section below). Generated on that Leaf, '$' prints 'Leaf(Base(id=1), name=leaf)': the generated '$' calls the NEAREST '$' above it (the super's, or one further up when the types in between add no fields) and then renders this type's own fields. Write the operator by hand instead when the fields must not all be shown or copied (good_written.ek9 below: it overrides Base's '$' and shows the name only).

For sameness the fix is one line (good_sameness.ek9 below): 'default operator <=>' below an ordered super, or 'default operator ==' below an equatable-only one. It compares the super's sameness first, then this type's own fields, and generates '#?' with it. A hand-written '<=>' or '==' needs a hand-written '#?' (E07184).

The bare 'default operator' (no operator named) declares all of them at once, with the same kind of sameness as the super.

Common causes

  • A field added to a sub-type that had none, so it used to inherit '$' safely
  • 'default operator ?' added for E07235 without the matching '$', '$$' or ':=:'
  • A copy (':=:') declared on the root of a hierarchy and not on its sub-types
  • 'default operator <=>' declared on the root of a hierarchy and not on the sub-types that add fields, an abstract one in between included

See also

  • E07235: a type with fields must define its own '?'
  • E07185: an abstract '$', '$$' or '#?' on a type with fields
  • E07190: 'default operator' with no such operator above it
  • E07182: hierarchy-wide sameness (written by hand on an abstract base or trait) is sealed
  • E07186: below an ordered super a sub-type's own sameness is '<=>'

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E07238: a type that adds fields, concrete or abstract, must declare its own '$', '$$' and ':=:', and its own sameness ('<=>' or '==', with '#?'), where it would inherit one: the inherited operator was written for the super's fields and cannot know these - use 'default operator' for each (or the bare 'default operator') or write one

#!ek9
defines module error.e07238.bad

  defines record

    //A record root with '$$' and ':=:'.
    Reading as open
      celsius as Float: Float()
      default operator ?
      default operator $$
      default operator :=:

    //Adds a field, declares '$$' only: it would inherit Reading's ':=:'.
    Located extends Reading
      place as String: String()
      default operator ?
      default operator $$

  defines class

    //A class root with every one of the three.
    Base as open
      id as Integer: Integer()
      default operator ?
      default operator $
      default operator $$
      default operator :=:

    //Adds a field and declares only '?': it would inherit all three.
    Leaf extends Base
      name as String: String()
      default operator ?

    //No fields of its own: needs nothing.
    Middle extends Base as open
      Middle()
        super()

    //Adds a field below the field-less middle: the nearest '$' is still Base's.
    Deeper extends Middle
      weight as Integer: Integer()
      default operator ?
      default operator $$
      default operator :=:

    //A field-less root whose '$' is abstract.
    Shape as abstract
      operator $ as pure abstract
        <- rtn as String?

    //Adds a field but declares no '$': below an abstract one the field would be left out of every sub-type's '$'.
    Named extends Shape as abstract
      label as String: String()
      default operator ?

    //An ordered root: the bare 'default operator' gives it '<=>' over score, and '#?'.
    Ordered as open
      score as Integer: Integer()
      default operator

    //Adds a field and declares every operator but its sameness: it would inherit Ordered's '<=>' and '#?'.
    Ranked extends Ordered
      name as String: String()
      default operator ?
      default operator $
      default operator $$
      default operator :=:

  defines component

    Store as open
      size as Integer: Integer()
      default operator ?
      default operator $

    FileStore extends Store
      path as String: String()
      default operator ?

Fixed: Declare the operator where the fields are added

#!ek9
defines module error.e07238.good

  defines class

    Base as open
      id as Integer: Integer()
      default operator ?
      default operator $

    //Adds a field, so it declares its own '$' over it
    Leaf extends Base
      name as String: String()
      default operator ?
      default operator $

Fixed: Declare the sameness where the fields are added

#!ek9
defines module error.e07238.good.sameness

  defines class

    Ordered as open
      score as Integer: Integer()
      default operator

    //Adds a field, so it declares its own '<=>' (and so '#?'): Ordered's first, then the name
    Ranked extends Ordered
      name as String: String()
      default operator ?
      default operator $
      default operator $$
      default operator :=:
      default operator <=>

Fixed: Or write the operator by hand when not every field should be shown

#!ek9
defines module error.e07238.good.written

  defines class
    Base as open
      id as Integer: Integer()
      default operator ?
      default operator $

    Leaf extends Base
      name as String: String()
      default operator ?

      //Shows the name only, by choice
      override operator $ as pure
        <- rtn as String: `Leaf ${name}`

Questions and answers

These Q&A entries show E07238 and its fix: