How do pure and non-pure methods coexist in an override hierarchy?
← Purity Contracts · Ref: Q678
A class can have both pure and non-pure methods. The purity contract applies per-method, not per-class. When overriding, pure methods must stay pure, and non-pure methods can remain non-pure.
PURE METHOD OVERRIDE (E05150)
If a parent method is declared as pure, every override in every descendant MUST also be pure. Removing purity from an override triggers E05150.
NON-PURE METHOD OVERRIDE
If a parent method is NOT pure, overrides are also not pure. A descendant CANNOT add purity to an override that was not pure in the parent (E05160).
MUTATION IN PURE (E08120)
Pure methods cannot mutate fields. Using ':=' on a field, or '+=' on a collection, inside a pure method triggers E08120.
CALLING NON-PURE FROM PURE (E08130)
A pure method cannot call a non-pure method. This transitively ensures that pure methods have no side effects.
PURE CONSTRUCTOR RULE (E05190)
If ANY constructor is pure, ALL constructors must be pure. Mixed constructor purity is not allowed.
See Q560 for pure method basics. See Q561 for purity override rules. See Q567 for purity chain inheritance. See Q566 for pure method restrictions.
Example
defines module qa.purity.overridecontract defines class <?- Base class with both pure and non-pure methods. Pure methods define read-only contract. Non-pure methods allow state mutation. Fields are always private; accessor methods expose state. -?> Account as open balance <- Float() accountName <- String() Account() -> accountName as String initialBalance as Float this.accountName: accountName this.balance: initialBalance //Pure accessor: subclasses use this to read balance getBalance() as pure <- rtn as Float: balance //Pure accessor: subclasses use this to read name getAccountName() as pure <- rtn as String: accountName //Pure: read-only computation formatSummary() as pure <- rtn as String: `${accountName}: ${balance}` //Non-pure: mutates balance field deposit() -> amount as Float balance: balance + amount //Non-pure: mutates balance field withdraw() -> amount as Float if amount <= balance balance: balance - amount default operator ? <?- Child class: pure overrides stay pure, non-pure stay non-pure. Each override maintains the parent contract. Accesses parent state via accessor methods (fields are private). -?> SavingsAccount extends Account interestRate <- Float() SavingsAccount() -> accountName as String initialBalance as Float interestRate as Float super(accountName, initialBalance) this.interestRate: interestRate //Pure override: stays pure, uses accessor methods for parent state override formatSummary() as pure <- rtn as String: `Savings ${getAccountName()}: ${getBalance()} @ ${interestRate}%` //Non-pure: adds interest via parent's deposit method applyInterest() earned <- getBalance() * interestRate / 100.0 deposit(earned) default operator ? defines program PurityOverrideDemo() stdout <- Stdout() savings <- SavingsAccount("Rainy Day", 1000.0, 2.5) stdout.println(savings.formatSummary()) savings.deposit(500.0) savings.applyInterest() stdout.println(savings.formatSummary())
Common mistakes
E05190 — If ANY constructor is declared pure, ALL constructors in the same class must be pure. Mixed constructor purity triggers E05190. See ek9 -h E05190 for details.
Incorrect:
Account() as pure -> accountName as String initialBalance as Float this.accountName :=? accountName this.balance :=? initialBalance Account() -> accountName as String this.accountName: accountName
Correct:
Account()
->
accountName as String
initialBalance as Float
this.accountName: accountName
this.balance: initialBalance
E08100 — Pure methods cannot mutate fields. Using ':=' on a field inside a pure method triggers E08100 because it constitutes a side effect. See ek9 -h E08100 for details.
Incorrect:
formatSummary() as pure <- rtn as String: "" balance: balance + 1.0 rtn: `${accountName}: ${$balance}`
Correct:
formatSummary() as pure <- rtn as String: `${accountName}: ${balance}`
E08130 — A pure method cannot call a non-pure method. Since deposit() mutates state, calling it from a pure method triggers E08130. See ek9 -h E08130 for details.
Incorrect:
override formatSummary() as pure <- rtn as String: "" deposit(100.0) rtn: `Savings ${getAccountName()}: ${$getBalance()} @ ${$interestRate}%`
Correct:
override formatSummary() as pure <- rtn as String: `Savings ${getAccountName()}: ${getBalance()} @ ${interestRate}%`
Other ways to ask this
- What is E05190 pure constructor mismatch?
- What happens when a pure override tries to mutate state?
- What happens when a pure method calls a non-pure method?
- Can a class have both pure and non-pure methods with overrides?
Coming from another language?
Java: no purity enforcement. Kotlin: no purity. Rust: shared references prevent mutation (different mechanism). Python: no purity. EK9: per-method purity with compile-time enforcement across the entire override chain.
Keywords: override, virtual, open, mutation, side-effect, E08120, E08130, immutable, abstract, purity, E05190, mixed, function, pure, non-pure, contract