Ownership · Lesson 11.3

Move

Source
Hand a value on with <- in a binding, an argument and a return, and see the old name become unusable.
You'll need: Copy

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:

WhereCopyMove
A bindinglet taken = value;let taken <- value;
An argumentRun(value)Run(<-value)
A returnreturn 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 afterwardstwooneone
The old namestill usablefinishedstill the owner
The new nameowns its own valueowns the valueowns 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.

Src/Main.rux
// 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

Using a name after moving from it.
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.
Moving inside a loop.
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.
Moving inside a branch, then using the name.
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

  1. Add a fourth runner, Dana, to the relay, and print the laps run.
  2. Uncomment the PrintLine after let first <- baton; and read the error and its note.
  3. Change Run(<-first, "Ana") to Run(first, "Ana"). The program still compiles — why? Print first.laps at the end: which baton does first still hold?
  4. Declare var baton, move it into first, then assign it a new Baton { 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