Skip to content
Pre-Release: Fe is under active development. This documentation covers the upcoming release. Follow progress on GitHub

Mutability in Effects

Effects distinguish between read-only and mutable access. This distinction is enforced at compile time, preventing accidental mutations.

By default, effects are read-only:

fn get_value() -> u256 uses (config: Config) {
config.value // Can read
}

Read-only effects:

  • Allow reading data from the effect
  • Prevent any modification
  • Are the default when mut is not specified

Attempting to modify a read-only effect is a compile error:

// This would be a compile error:
fn try_modify() uses (config: Config) {
config.value = 100 // Error: cannot modify immutable effect
}

Add mut to allow modification:

fn set_value(new_value: u256) uses (mut config: Config) {
config.value = new_value // Can modify
}

Mutable effects:

  • Allow both reading and writing
  • Must be explicitly declared with mut
  • Require a mutable binding when provided

When calling a function that requires an effect, the caller must provide a compatible effect:

A mutable effect can satisfy an immutable requirement:

fn read_only() uses (data: Data) {
// reads Data
}
fn outer() uses (mut data: Data) {
read_only() // OK: mut Data satisfies Data
}

An immutable effect cannot satisfy a mutable requirement:

// This would be a compile error:
fn needs_mut() uses (mut data: Data) {
// modifies Data
}
fn outer() uses (data: Data) {
needs_mut() // Error: Data cannot satisfy mut Data
}

When providing an effect with with, the binding’s mutability determines the effect’s mutability:

pub struct Counter { pub value: u256 }
fn needs_mut() uses (mut counter: Counter) {
counter.value = counter.value + 1
}
fn example() {
// Immutable binding - would error if we tried to call needs_mut()
let counter = Counter { value: 0 }
// Mutable binding - can satisfy mut effect
let mut counter2 = Counter { value: 0 }
with (Counter = counter2) {
needs_mut() // OK: counter2 is mutable
}
}
  • The function only reads data
  • You want to prevent accidental modification
  • You’re implementing a getter or query
fn total_supply() -> u256 uses (store: TokenStore) {
store.supply
}
fn is_valid(amount: u256) -> bool uses (config: Config) {
amount <= config.max_amount
}
  • The function modifies data
  • You’re implementing a setter or state change
  • The operation has side effects
fn mint(amount: u256) uses (mut store: TokenStore) {
store.supply = store.supply + amount
}
fn update_config(new_max: u256) uses (mut config: Config) {
config.max_amount = new_max
}

Named effects follow the same rules:

fn process() uses (data: Data, mut cache: Cache) {
// data is read-only
// cache is mutable
let value = data.value // OK: reading
cache.store(value) // OK: mutable operation
// data.value = 100 // Would error: data is not mutable
}

A common pattern separates read and write operations:

pub struct Balances {
pub data: Map<u256, u256>,
}
// Read-only: safe to call anywhere
fn get_balance(account: u256) -> u256 uses (balances: Balances) {
balances.data.get(account)
}
// Mutable: changes state
fn set_balance(account: u256, amount: u256) uses (mut balances: Balances) {
balances.data.set(account, amount)
}
// Mutable: combines read and write
fn transfer(from: u256, to: u256, amount: u256) uses (mut balances: Balances) {
let from_balance = get_balance(from) // Calls read-only function
let to_balance = get_balance(to)
set_balance(from, from_balance - amount)
set_balance(to, to_balance + amount)
}
DeclarationCan ReadCan Modify
uses (e: Effect)YesNo
uses (mut e: Effect)YesYes
Caller HasCallee NeedsResult
(e: Effect)(e: Effect)OK
(mut e: Effect)(e: Effect)OK
(e: Effect)(mut e: Effect)Error
(mut e: Effect)(mut e: Effect)OK