A drawing program shows one document in several places at once: the canvas where you drag shapes around, a layers panel that lists them, perhaps a pane of properties. Each view holds the document. Each may change it. Each must see the others' changes at once.
It is one of the most ordinary shapes in software. A widget that knows its parent. A list of observers that all hold the thing they watch. A game creature that keeps hold of the one it is chasing. Many hands on one value, any of them free to change it. Here is the drawing program in Cone:
// Two views of one document. Each holds it, and either may change it
struct Canvas {
pub doc: Rc[mut, Doc];
fn drag(self: &mut, i: usize, dx: i32) {
let s = &mut doc.shapes[i]; // a borrow into the document's list
s.x += dx;
doc.edits += 1; // the document is written while s is held
s.y += 1;
}
}
struct Layers {
pub doc: Rc[mut, Doc];
fn add(self: &mut, name: &str) {
doc.shapes.push(Shape(name, 0, 0));
doc.edits += 1;
}
}
let doc = Rc[mut, Doc](List[Shape].empty(), 0);
var canvas = Canvas(doc);
var layers = Layers(doc);
layers.add("circle");
layers.add("square");
canvas.drag(1usize, 40);
The layers panel adds two shapes, the canvas drags one, and when the panel lists them it sees the square moved and three edits made. Nothing was copied, wrapped or looked up by number. Both views simply hold the document.
The word doing the work is mut. Every reference in Cone carries a permission, a word saying what it may do. Rc[mut, Doc] is a counted owner of the document whose permission is mut: shared and mutable. Any number of mut references may point at one value, and any of them may change it, at any time. Inside drag, s is a borrow straight into the document's list of shapes, and the document is written while s is still in use.
Why it's safe
Shared, changeable data goes wrong in two ways, and Cone closes both when it compiles.
The first is another thread. If two threads change one value at the same moment, it can end up half one change and half the other: a data race. A mut reference never leaves the thread it lives on. Try to hand the document to a background autosave thread, and the compiler says so, and says what may cross:
let saver = thread.start(doc, &autosave); // Error 1194: start requires A is Sendable, and Rc[mut, Doc] is not Sendable: // it is an owner whose permission, mut, is not race-safe: only a uni, imm or opaq // reference may be shared with or sent to another thread.
The second is subtler, and happens on one thread. A list keeps its elements in one block of memory, and adding to it may move them all to a bigger block. A borrow into the old block would then point at freed memory. So Cone watches for shape changes: a push, a clear, a new variant stored over an enum. Writing an element in place is fine, and so is reading or writing the rest of the document. What is refused is a shape change while a borrow into the value is still to be used. Add a shape in the middle of the drag, and:
let s = &mut doc.shapes[i];
s.x += dx;
doc.shapes.push(Shape("copy", 0, 0));
// Error 1339: This call could change the shape of a 'List' while 's' still holds
// a borrow into it ...
s.y += 1;
The fix is to finish with s first, or copy out what you need. The compiler works out which calls can change a shape from the types' own methods, so your own types are covered too, with nothing to declare.
Why it performs
All of this is settled before the program runs. A permission is checked by the compiler alone, so it costs nothing while the program runs: no flag set and tested on each access, no lock, no copying values in and out. s is a plain pointer into the list, and writing through it is a plain write. The counted owner still counts its copies, which is what Rc is for, but borrows like s count nothing.
Why it matters as the code changes
Next month there is a properties pane. It is one more struct holding the same Rc[mut, Doc]. Nothing else is restructured; the document's shape is still the shape of the problem.
And the checks keep watch as the code grows. When someone adds a push in the middle of a borrow, the compiler points at the line, while the change is still fresh in mind, not after a crash in a user's hands. When someone moves the autosave to another thread, it stops them there, and names the permissions that may cross. You keep the freedom to share; the compiler keeps the bookkeeping.
Try it
These links point at the Cone manual:
- Permissions: what a reference may do, including uni, imm and mut.
- Borrowing into a collection or an enum: shape changes.
- Borrowed references and Reference counting.
- What may cross a thread.
Why Cone · Previous: Memory, value by value · Next: Actors and parallel each