Why must an override of a pure method also be pure?

← Purity Contracts · Ref: Q561

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

When a parent method is marked 'as pure', every override of that method must also be marked 'as pure'. The compiler enforces this with error E05150.

WHY THIS RULE EXISTS

Callers of the parent type expect pure behavior. If a child could drop purity, polymorphic calls would break the contract:

  parent as Shape: Circle()
  parent.area()  // Caller expects pure, child must honour that

THE ERROR: E05150

If the parent method is pure and the child override is not pure, the compiler reports E05150: the parent method is pure so the override must also be pure.

CORRECT PATTERN

Always add 'as pure' to the override when the parent is pure:

  override area() as pure
    <- rtn as Float: ...

BEHAVIORAL SUBSTITUTION

This enforces the Liskov Substitution Principle for purity. Anywhere a Parent is used, a Child can replace it without violating purity expectations.

See Q560 for pure method basics. See Q562 for the opposite rule (cannot add pure). See Q567 for purity through inheritance chains. See Q568 for pure operators and overrides.

Example

defines module qa.purity.overriderequired

  defines class

    Shape as abstract
      //Pure method in parent: all overrides must also be pure
      area() as pure abstract
        <- rtn as Float?

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

      default operator ?

    Circle extends Shape
      radius <- 0.0

      Circle()
        -> radius as Float
        this.radius: radius

      //Correct: override of pure method is also pure
      override area() as pure
        <- rtn as Float: radius * radius * 3.14159

      override describe() as pure
        <- rtn as String: `Circle with radius ${radius}`

      default operator ?

    Square extends Shape
      side <- 0.0

      Square()
        -> side as Float
        this.side: side

      //Correct: pure override matches parent purity
      override area() as pure
        <- rtn as Float: side * side

      override describe() as pure
        <- rtn as String: `Square with side ${side}`

      default operator ?

  defines program

    PureOverrideRequiredDemo()
      stdout <- Stdout()

      //Polymorphic use: callers trust pure contract
      shapes <- List() of Shape
      shapes += Circle(5.0)
      shapes += Square(4.0)

      for shape in shapes
        stdout.println(`${shape.describe()}: area = ${shape.area()}`)

Common mistakes

E05150 — The parent Shape declares area as pure abstract. Overriding without as pure violates the purity contract. All overrides of pure methods must also be pure. See ek9 -h E05150 for details.

Incorrect:

override area()

Correct:

override area() as pure

E05120 — When the parent Shape has an abstract describe method, the child must use the override keyword to replace it. Omitting override triggers shadowing detection. See ek9 -h E05120 for details.

Incorrect:

describe() as pure

Correct:

override describe() as pure
Other ways to ask this
  • What is E05150 SUPER_IS_PURE?
  • What happens if I override a pure method without 'as pure'?
  • How does purity work with method overriding?

Coming from another language?

Java: no purity concept, method contracts are documentation only. Kotlin: no purity enforcement. Rust: trait methods have no purity marker. EK9: compiler-enforced purity inheritance via E05150.

Keywords: open, function, purity, override, child, parent, inheritance, abstract, immutable, pure, contract, substitution, E05150, side-effect, virtual