Move
A copy leaves two values behind. A move leaves one: the value is handed to its new owner, and the old name is finished. Rux writes a move as <-, the arrow pointing the way the value goes.
A relay baton is the picture to keep in mind. One runner holds it at a time, and once it has been handed on, the runner who passed it no longer has it. The compiler enforces exactly that: any later use of a moved-from name is an error.
For a Baton holding one number, the point of a move is not speed. It is what is left afterwards — and in the lessons after this one, that becomes the difference between a value cleaned up once and a value cleaned up twice.
Three places to move
A move works in the same three places a copy happens. Only the arrow changes:
| Where | Copy | Move |
|---|---|---|
| A binding | let taken = value; | let taken <- value; |
| An argument | Run(value) | Run(<-value) |
| A return | return value; | return <-value; |
A binding
let baton = Baton { laps: 0 };
let first <- baton;
first now holds the baton. baton still exists as a name in the source, but it no longer holds anything, and the compiler will not let you read it:
error: value 'baton' is used after it was moved
note: 'baton' was moved at 31:15
help: clone 'baton' before moving it if both uses are required
An argument and a return
Run takes the baton, runs one lap with it, and hands it back:
func Run(baton: Baton, runner: char8[..]) -> Baton {
var held <- baton;
held.laps += 1;
PrintLine("{} runs lap {}", runner, held.laps);
return <-held;
}
A parameter cannot be changed, so var held <- baton moves it into a mutable local: still one baton, just under a new name. return <-held hands it on to the caller.
The caller passes the baton along the line of runners:
let second = Run(<-first, "Ana");
let third = Run(<-second, "Ben");
let last = Run(<-third, "Cleo");
Each <- gives up a name: after the first line, first is finished, after the second, second, and so on. One baton runs three laps, and last.laps is 3.
Fresh values need no arrow
What a call returns is a fresh value that nobody else holds, so = simply takes it — there is no old name to give up, and nothing to copy from. let second = Run(…) above is already a move in all but spelling.
The same is true of any value made on the spot:
let spare = Run(Baton { laps: 0 }, "Dev");
The arrow is for a value that has a name. It is there so that a reader can see, at a glance, which names stop working on that line.
Copy, move or borrow
You now have three ways to hand a value to someone else:
flowchart LR
a(["a: a value<br/>with a name"])
a -- "let b = a<br/>copy" --> c["two values<br/>a and b both usable,<br/>each changes alone"]
a -- "let b <- a<br/>move" --> m["one value, now b's<br/>a is finished"]
a -- "let b: &T = a<br/>borrow" --> r["one value, still a's<br/>b refers to it"]Copy = | Move <- | Borrow & / &var | |
|---|---|---|---|
| Values afterwards | two | one | one |
| The old name | still usable | finished | still the owner |
| The new name | owns its own value | owns the value | owns nothing; refers |
Moves on some paths
The compiler tracks moves through loops and branches. A move inside a loop is checked against the next pass too — the first pass would take the baton, and the second would find nothing left to take:
for lap in 0..3 {
let runner <- first;
}
error: value 'first' may have been moved on some control-flow paths
The same error appears when a move happens inside an if and the name is used after it: on the path where the if ran, the value is gone.
A moved-from var is not ruined for good. Assigning it a new value with = gives it something to hold again, and from then on it can be read as before.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A copy leaves two values behind. A move leaves one: the value is handed to its new owner and
// the old name is finished. Rux writes a move as `<-`, the arrow pointing the way the value goes,
// and it works in the same three places a copy happens.
//
// let taken <- value; a binding
// Run(<-value) an argument
// return <-value; a return
//
// A relay baton is the picture to keep in mind. One runner holds it at a time, and once it has
// been handed on, the runner who passed it no longer has it. The compiler enforces exactly that:
// any later use of a moved-from name is an error.
import Io::PrintLine;
struct Baton {
laps: int32;
}
// Takes the baton, runs one lap with it, and hands it back. A parameter cannot be changed, so
// `var held <- baton` moves it into a mutable local: still one baton, just under a new name.
func Run(baton: Baton, runner: char8[..]) -> Baton {
var held <- baton;
held.laps += 1;
PrintLine("{} runs lap {}", runner, held.laps);
return <-held;
}
func Main() -> int {
let baton = Baton { laps: 0 };
// A binding takes the baton over.
let first <- baton;
// From here on, `baton` cannot be used:
//
// PrintLine("{}", baton.laps);
//
// error: value 'baton' is used after it was moved
// note: 'baton' was moved at 31:15
// help: clone 'baton' before moving it if both uses are required
//
// A move inside a loop is checked against the next pass too. The first pass would take the
// baton, and the second would find nothing left to take:
//
// for lap in 0..3 {
// let runner <- first;
// }
//
// error: value 'first' may have been moved on some control-flow paths
// Arguments and returns. What a call returns is a fresh value that nobody else holds, so it
// needs no arrow: `=` simply takes it.
let second = Run(<-first, "Ana");
let third = Run(<-second, "Ben");
let last = Run(<-third, "Cleo");
PrintLine("laps run: {}", last.laps);
// The same is true of any value made on the spot. There is no old name to give up.
let spare = Run(Baton { laps: 0 }, "Dev");
PrintLine("spare laps: {}", spare.laps);
return 0;
}
Run it
cd Examples/Ownership/Move
rux run
Ana runs lap 1
Ben runs lap 2
Cleo runs lap 3
laps run: 3
Dev runs lap 1
spare laps: 1
Common mistakes
After
let first <- baton;, PrintLine("{}", baton.laps) fails with error: value 'baton' is used after it was moved. The note points at the line of the move. If both names really need a value, copy with = instead of moving.let runner <- first; in a loop body fails with error: value 'first' may have been moved on some control-flow paths — the second pass would have nothing to take. Move before the loop, or give each pass a value of its own.if ready { let first <- baton; } followed by baton.laps fails the same way: error: value 'baton' may have been moved on some control-flow paths. The compiler does not know whether the if ran, so it assumes the worst.Try it yourself
- Add a fourth runner, Dana, to the relay, and print the laps run.
- Uncomment the
PrintLineafterlet first <- baton;and read the error and its note. - Change
Run(<-first, "Ana")toRun(first, "Ana"). The program still compiles — why? Printfirst.lapsat the end: which baton doesfirststill hold? - Declare
var baton, move it intofirst, then assign it a newBaton { laps: 7 }and print both.
Learn more
- Copy — the other way a value is handed on
- Destructor — the next lesson, where a move decides who cleans up
- Non-copyable types — values that can only be moved