ADR-059: access Reads and update Writes, and the One Block That Changes a Value Says mut

Status: Superseded by ADR-281 — its D1 is D11 there, and D2 and D3 are D12 Supersedes: ADR-039 D10’s access row — “in place, large values”, whose block mutated through its parameter Related: ADR-057 (what Locked[T] is; this is what a program writes to reach one), ADR-049 (the one lambda form this keeps), Part I 5.3 (that form), Part II 12.2 (the four doors)

1. Context

ADR-039 D10 gave locked data four doors, and two take a lambda. update is settled by its own row — handed the old value, returns the new one. access was not: its block mutates through its parameter (to.balance += 100), and a lambda’s parameter has no spelling that says it may be. The open question was which spelling to add, or whether to let mutability arrive invisibly from the callee’s contract the way a parameter’s type already does.

Two facts, looked up rather than assumed, decide it.

So an invisible answer would be the first place in this language where something is changed and the line does not say so, and the visible answer means a second lambda form, which is what ADR-049 was written to protect.

And the question turned out to rest on a cost that is not there. access’s whole justification is that copying is too expensive — “a list of ten thousand entries is not copied to append one”. Run:

fn grow(old: Vec[i64]) -> Vec[i64] {
    let mut v = old
    v.push(4)
    return v
}
fn grow(old: Vec<i64>) -> Vec<i64> {
    let mut v = old;
    v.push(4);
    v
}

That is a move: three words, whether the list holds ten entries or ten thousand. update can already append without copying anything, in ordinary Nikaia, with the mutation spelled by the word this language already has, in the place it already has it. The door that needed a new spelling was earning nothing that the door without one could not.

What update cannot do is read part of a large value: it must return the new state, and get() copies. Asking a shared list for its length would copy the list.

2. Decisions

D1 — access reads in place and may not change what it is handed.

kasse.access fn(state) { state.len() }

It keeps its name and its purpose — in place, large values — and loses the mutation. What it hands the lambda is the value where it lies, to read; what it returns is what the lambda computed.

This is the one thing neither other door does, and it is why access stays rather than going: without it, asking a ten-thousand-entry list for its length copies the list.

D2 — update is where locked data changes, and its block says mut.

kasse.update fn(mut v) { v += 100 }
kasse.update fn(mut v) { v.push(entry) }

The block takes the value as mut v, changes it and returns nothing (ADR-110 D1). Whether v is a copy or the address of the value in the lock is the compiler’s, by type, and is what decides whether the block may be retried instead of locked (D2 and D3 of that record).

D3 — Therefore exactly one lambda is handed something it may change, and it is spelled mut.

The word is ADR-094 D3’s, with its meaning unchanged: a parameter written mut is changed in place, and the caller’s value — here the lock’s — is what changes. Part I 5.3’s one lambda form is untouched, and a reader sees a change where it happens.

3. Consequences

One row of Part II 12.2’s table changes, and the sentence naming the doors that require a sync lambda keeps all of them: access still runs user code while the lock is open, so it still may not pause, and reading is no weaker a reason than writing.

get and set are untouched, including the reason they need no condition at all: while the lock is open in either, no code of the user’s runs.

Two accesses to one lock keep their order, and access moves sides. ADR-039 D10 reasoned that get is a read and set, update and access are writes, so that two gets are unordered under ADR-033 D2 and the three writing forms stay ordered. access is a read now, so two accesses of one lock are unordered too, and the writing forms are set and update.

What it costs: a program that changed a large value in place writes update and the word mut. The word is the change, said out loud.

4. What this does not decide

5. What is built

All three decisions, on top of ADR-057’s two shapes.

nikaia_std::lock carries all four doors on both shapes: get, set, access taking &T, and update — still taking T and returning one, with the value in an Option<T> so that it can be moved out and back; D2’s mut form, and the copy-or-address choice behind it, are ADR-110’s and are listed in that record’s §5. std.contracts describes all four, each sync = "from(f)" where it takes a lambda.

This site is open source. Improve this page.