What happens when a method comes from both a pure parent and a non-pure parent?

← Purity Contracts · Ref: Q1401

'pure' is always part of a method's contract, so an override is checked against EVERY parent that declares the method - the super class and each trait. When all of them are pure the override must be pure (E05150); when none are, it must not be (E05160). When they DISAGREE - a non-pure super and a pure trait, or two traits one way and the other - no single method can honour both contracts, and the class is rejected with E05290.

WHY

A caller holding the pure trait relies on the method having no side effects; a caller holding the non-pure super relies on it being free to have them. One body cannot be both.

FIX

Make the parents agree - mark both declarations 'pure', or neither - or stop combining them on one type.

This differs from 'sanitized', which an override may ADD but never drop: 'pure' must match every parent exactly.

See Q561 for pure overrides. See Q569 for traits and pure contracts. See Q663 for how 'sanitized' differs.

Example

defines module qa.purity.multipleparents

  defines trait

    <?-
      A pure contract: label() has no side effects.
    -?>
    Labelled
      label() as pure abstract
        <- rtn as String?

  defines class

    <?-
      The super declares label() pure too, so the parents agree.
    -?>
    Shape as open
      label() as pure
        <- rtn as String: "shape"

      default operator ?

    <?-
      Circle stands in for BOTH Shape.label() and Labelled.label() - it must be pure.
    -?>
    Circle extends Shape with trait of Labelled
      override label() as pure
        <- rtn as String: "circle"

      default operator ?

  defines program

    MultipleParentsDemo()
      stdout <- Stdout()
      asShape as Shape: Circle()
      asLabelled as Labelled: Circle()
      stdout.println(asShape.label())
      stdout.println(asLabelled.label())

Common mistakes

E05290 — Shape.label() would no longer be pure while the trait Labelled.label() is, so Circle's override would have to honour two contracts that disagree. See ek9 -h E05290 for details.

Incorrect:

label()
        <- rtn as String: "shape"

Correct:

label() as pure
        <- rtn as String: "shape"
Other ways to ask this
  • What is E05290 parents disagree on pure?
  • Can a class extend a non-pure super and implement a pure trait with the same method?
  • Is 'pure' checked against every trait, or only the first parent?
  • Why does my override conflict with two parents about purity?

Coming from another language?

Java: no purity in the type system - interfaces cannot promise side-effect freedom. Kotlin/Scala: no enforced purity. Haskell: purity is global (IO in the type), so the conflict cannot arise. EK9: 'pure' is part of every declaration's contract and the compiler rejects a type whose parents disagree on it.

Keywords: super, contract, parents, trait, override, pure, multiple, E05150, purity, conflict, E05290