What does 'as pure' mean on a method in EK9?

← Purity Contracts · Ref: Q560

The 'as pure' modifier on a method guarantees that it will not mutate any state through mutation operators. This is a compiler-enforced contract, not a convention.

WHAT PURE MEANS

A pure method cannot use mutation operators (+=, -=, *=, /=, :=:, :~:, :^:). Inside a pure scope only the declared return variable may be assigned again; any other variable is declared once with '<-' (or assigned only-if-unset with ':=?'), and reassigning it with ':' or ':=' is E08100. It cannot call non-pure methods. The compiler enforces all of these rules.

HOW TO DECLARE PURE

Add 'as pure' after the method name:

  calculate() as pure
    -> x as Integer
    <- rtn as Integer: x + 1

WHAT PURE ALLOWS

Assigning the return variable (rtn: working + 1). Declaring new locals with '<-'. Creating new values with non-mutating operators (+, -, *, /). Reading fields and parameters. Returning computed values.

WHAT PURE FORBIDS

Mutation operators (+=, -=, *=, /=, :=:, :~:, :^:) are forbidden (E08120), even on the return variable. Reassigning any variable other than the return variable with ':' or ':=' is forbidden (E08100). Calling non-pure functions or methods is forbidden (E08130). These are compile-time errors, not runtime checks.

WHY PURE MATTERS

Pure methods are safe to call from any context. They cannot corrupt shared state. They enable compiler optimizations. They create auditable security boundaries.

See Q273 for purity as a security boundary. See Q54 for pure functions and Consumer vs Acceptor. See Q561 for purity inheritance rules. See Q566 for pure call chain restrictions. See Q612 for dispatcher purity matching requirements. See Q635 for forbidden mutation operators in pure methods.

See Q666 for injection in pure context.
See Q678 for purity override contract. See Q690 for pure call chain.

Example

defines module qa.purity.basics

  defines class

    Calculator
      factor <- 1

      Calculator()
        -> factor as Integer
        this.factor: factor

      //A pure method: no mutation, just computation
      compute() as pure
        -> inputValue as Integer
        <- result as Integer: inputValue * factor

      //A pure method may assign its own return variable, but no other variable;
      //a local is declared once with '<-' and never reassigned
      offsetBy() as pure
        -> inputValue as Integer
        <- rtn as Integer: 0
        working <- inputValue * factor
        rtn: working + 1

      //A pure accessor: reads state without modifying it
      currentFactor() as pure
        <- rtn as Integer: factor

      //Non-pure method: uses mutation operator
      adjustFactor()
        -> delta as Integer
        factor += delta

      default operator ?

  defines program

    PureMethodBasicsDemo()
      stdout <- Stdout()

      calc <- Calculator(3)
      stdout.println(`Factor: ${calc.currentFactor()}`)

      result <- calc.compute(10)
      stdout.println(`3 * 10 = ${result}`)
      stdout.println(`3 * 10 + 1 = ${calc.offsetBy(10)}`)

      //Non-pure: mutates factor
      calc.adjustFactor(2)
      stdout.println(`After adjust: ${calc.currentFactor()}`)

      result2 <- calc.compute(10)
      stdout.println(`5 * 10 = ${result2}`)

Common mistakes

E08100 — In a pure method only the declared return variable may be assigned again. Reassigning the local 'working' is E08100; compute into the return variable instead (or declare a new local with '<-'). See ek9 -h E08100 for details.

Incorrect:

        working: working + 1

Correct:

        rtn: working + 1

E08120 — Mutation operators such as '+=' are forbidden in a pure scope, even on the return variable. Assign a newly computed value with ':' instead. See ek9 -h E08120 for details.

Incorrect:

        rtn += working + 1

Correct:

        rtn: working + 1
Other ways to ask this
  • How do I mark a method as pure in EK9?
  • What restrictions apply to pure methods?
  • How does EK9 enforce method purity?

Coming from another language?

Java: no purity concept, any method can mutate anything. Python: no purity enforcement. Haskell: all functions pure by default (IO monad for effects). Rust: immutable borrows prevent mutation but no purity marker. Kotlin: no purity concept. EK9: 'as pure' modifier enforces no mutation operators, no reassignment except of the return variable, and no non-pure calls at compile time.

Keywords: contract, immutable, mutation, purity, side-effect, pure, method, enforce, operator