What operations are forbidden inside a pure method?

← Data Flow Safety · Ref: Q635

Pure methods and functions are forbidden from using any mutation operator. This is enforced at compile time as E08120.

FORBIDDEN MUTATION OPERATORS IN PURE CONTEXT

The following operators are mutation operators and cannot appear inside a pure method or function: += (add-assign), -= (subtract-assign), *= (multiply-assign), /= (divide-assign), :=: (copy-into), :~: (merge-into), :^: (replace-with). Using any of these inside a pure method triggers E08120.

ALSO FORBIDDEN: REASSIGNMENT

Reassigning an existing local variable with : (or :=) inside a pure scope is rejected as E08100. Reassigning a parameter is E08110, in any scope.

ALLOWED IN PURE CONTEXT

Declaring a fresh variable (x <- expr) creates a new binding and is allowed. Assigning the return variable is allowed. Assign-if-unset (:=?) is allowed. Non-mutating operators (+, -, *, /, ==, <>) are allowed. Creating new values and returning them is allowed. Reading fields and parameters is allowed.

WHY THIS RESTRICTION EXISTS

Pure methods guarantee no side effects. If a pure method could use +=, it would mutate the state of an existing object, breaking the purity contract. The compiler enforces this so callers can trust that pure methods do not change any state.

NEW BINDINGS VS MUTATION

Mutation (variable += delta) modifies the existing object in place: E08120. Reassignment (variable: newValue) is also refused in a pure scope: E08100. Write the computation as fresh declarations (next <- current + delta) or as the initialiser of the return variable, as projectedBalance does.

See Q560 for pure method basics. See Q566 for pure call chain restrictions. See Q273 for purity as a security boundary.

Example

defines module qa.dataflow.puremutation

  defines class

    Account
      balance <- 0.0
      accountName <- "unnamed"

      Account()
        ->
          accountName as String
          initialBalance as Float
        this.accountName: accountName
        this.balance: initialBalance

      <?-
        Pure: computes new balance without mutating the field.
        Uses the + operator (non-mutating) in the return variable's initialiser;
        no variable is reassigned.
      -?>
      projectedBalance() as pure
        -> deposit as Float
        <- projected as Float: balance + deposit

      <?-
        Pure: builds a formatted string from fields.
        No mutation operators used.
      -?>
      formatSummary() as pure
        <- summary as String: `${accountName}: ${balance}`

      <?-
        Non-pure: uses mutation operator to modify balance.
        This method correctly omits 'as pure'.
      -?>
      deposit()
        -> amount as Float
        balance += amount

      <?-
        Non-pure: uses mutation operator.
      -?>
      withdraw()
        -> amount as Float
        balance -= amount

      default operator ?

  defines program

    PureMutationDemo()
      stdout <- Stdout()

      acct <- Account("Savings", 1000.0)
      stdout.println(acct.formatSummary())

      //Pure call: safe, no mutation
      projected <- acct.projectedBalance(500.0)
      stdout.println(`Projected: ${projected}`)

      //Non-pure: mutates the account
      acct.deposit(500.0)
      stdout.println(acct.formatSummary())

      acct.withdraw(200.0)
      stdout.println(acct.formatSummary())

Common mistakes

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

      stdout.println(acct.formatSummary().toUpperCase())

      //Pure call: safe, no mutation

Correct:

      stdout.println(acct.formatSummary())

      //Pure call: safe, no mutation

E08120 — A pure method may not use a mutation operator: 'balance += deposit' would change the account's state. Compute the new value with the non-mutating + instead. See ek9 -h E08120 for details.

Incorrect:

        <- projected as Float: balance + deposit
        balance += deposit

Correct:

        <- projected as Float: balance + deposit
Other ways to ask this
  • Why does EK9 reject mutation operators in pure methods?
  • What is E08120 mutation in pure context?
  • Can I use += inside a pure function?

Coming from another language?

Java: no compile-time purity enforcement. Python: no immutability guarantees. Haskell: all functions pure by default. Rust: mutable borrows are explicit. Kotlin: val prevents reassignment but does not prevent method-level mutation. EK9: E08120 rejects mutation operators and E08100 rejects reassignment in pure methods at compile time.

Keywords: immutable, E08120, operator, side-effect, mutation, migrate, restriction, assign, initialize, data-flow, safety, copy, pure, forbidden, merge, function