How does purity flow through multi-level inheritance chains?

← Purity Contracts · Ref: Q567

Note: override applies to both methods and operators (e.g., override operator ? as pure, override operator $ as pure).

Purity flows through the entire inheritance chain. If a grandparent method is pure, the parent override must be pure, and the grandchild override must also be pure. Every level must maintain the contract.

MULTI-LEVEL RULE

Grandparent declares method as pure. Parent override must be pure (E05150 if not). Grandchild override must be pure (E05150 if not). The chain never breaks.

WHY EVERY LEVEL MATTERS

Code using the grandparent type expects purity. The actual runtime object might be any descendant. Every descendant must honour the original contract.

PRACTICAL PATTERN

Design purity at the root of the hierarchy:

  Animal as abstract
    sound() as pure abstract
  Dog extends Animal
    override sound() as pure
  Labrador extends Dog
    override sound() as pure

CANNOT ADD PURE LATER

If the grandparent is NOT pure, no descendant can add purity (E05160). Purity is designed in at the top.

See Q561 for purity override rules. See Q562 for cannot-add-pure. See Q575 for deep override chains. See Q568 for pure operators.
See Q677 for abstract implementation chain. See Q678 for purity override contract. See Q699 for return type covariance.

Example

defines module qa.purity.chaininheritance

  defines class

    //Level 1: declares pure contract
    Animal as abstract
      sound() as pure abstract
        <- rtn as String?

      describe() as pure abstract
        <- rtn as String?

      default operator ?

    //Level 2: must maintain pure
    Dog extends Animal as open
      override sound() as pure
        <- rtn as String: "Woof"

      override describe() as pure
        <- rtn as String: "Dog"

      default operator ?

    //Level 3: must STILL maintain pure
    Labrador extends Dog
      override sound() as pure
        <- rtn as String: "Woof woof!"

      override describe() as pure
        <- rtn as String: "Labrador"

      default operator ?

    //Another chain: Cat hierarchy
    Cat extends Animal as open
      override sound() as pure
        <- rtn as String: "Meow"

      override describe() as pure
        <- rtn as String: "Cat"

      default operator ?

    Siamese extends Cat
      override sound() as pure
        <- rtn as String: "Mrow!"

      override describe() as pure
        <- rtn as String: "Siamese"

      default operator ?

  defines program

    PureChainDemo()
      stdout <- Stdout()

      //All levels honour the pure contract
      animals <- List() of Animal
      animals += Labrador()
      animals += Siamese()

      for animal in animals
        stdout.println(`${animal.describe()} says: ${animal.sound()}`)

Common mistakes

E05150 — At every level of the inheritance chain, the pure contract must be maintained. The abstract Animal.sound is pure, so Dog.sound and Labrador.sound must also be pure. See ek9 -h E05150 for details.

Incorrect:

override sound()

Correct:

override sound() as pure

E50050 — Even though describe is correctly declared as pure, calling a non-pure method from within it would violate the purity contract transitively. See ek9 -h E50050 for details.

Incorrect:

override describe() as pure
        <- rtn as String: nonPureHelper()

Correct:

override describe() as pure
Other ways to ask this
  • Does purity apply to grandchild classes?
  • How does the pure contract carry through three levels of inheritance?
  • Must every level maintain purity?

Coming from another language?

Java: no multi-level purity tracking. Kotlin: no purity. EK9: purity contract is enforced at every level in the inheritance chain via E05150.

Keywords: multi-level, pure, grandchild, inheritance, contract, side-effect, grandparent, chain, E05150, purity, immutable