Ownership · Lesson 11.7

Partial move

Source
See why a single field cannot be moved out of a struct, and take the struct apart with <- to move it out instead.

A parcel is wrapping paper around a gift. It is tempting to move just the gift out and leave the rest where it is — but Rux refuses: a field cannot be moved out of a value on its own. What works instead is taking the whole value apart, so that every field gets exactly one new owner.

This lesson brings together three things from earlier: move-only types, destructors, and the struct pattern you used to destructure values in let.

The parcel

Item is move-only and announces its own destruction. Parcel holds two of them and has no destructor of its own:

struct Item {
    name: char8[..];
}

extend Item {
    func =(self: &var Item, other: &Item);

    func ~Item(self: &var Item) {
        PrintLine("    {} destroyed", self.name);
    }
}

// No destructor of its own, so a moving pattern may take it apart.
struct Parcel {
    wrapping: Item;
    gift: Item;
}

Because both fields are move-only, Parcel is move-only too: copying a parcel would copy the items inside it.

One field on its own is refused

The obvious way to get the gift out does not compile:

return <-parcel.gift;
error: cannot move field 'gift' out of droppable value 'parcel'
  note: partial moves would leave the aggregate with only some fields initialized
  help: move the complete aggregate or borrow or clone the component explicitly

Half a parcel would be a value that is partly alive. When parcel's scope ended, the compiler could no longer say what to destroy: the wrapping, yes, but not the gift, which has moved on. Rather than track a value that is part here and part gone, Rux keeps the rule simple — a value is moved whole or not at all.

Taking the whole value apart

A struct pattern with <- moves the parcel into the pattern, and every field gets exactly one owner again:

func Unwrap(parcel: Parcel) -> Item {
    let Parcel { wrapping: _, gift: gift } <- parcel;
    PrintLine("    unwrapped the {}", gift.name);
    return <-gift;
}
  • A field bound to a name belongs to that binding. gift is a local Item now, and return <-gift hands it to the caller.
  • A field bound to _, or left out of the pattern, belongs to nobody, so it is destroyed on the spot.

That is why the output reads "paper destroyed" before "unwrapped the book": the wrapping is gone by the time the let finishes.

flowchart LR
    p(["`parcel: Parcel`"]) -- "`let Parcel { … } <- parcel`" --> pat{"each field<br/>gets one owner"}
    pat -- "`wrapping: _`" --> w["no owner —<br/>destroyed at the `let`"]
    pat -- "`gift: gift`" --> g["owned by `gift`"]
    g -- "`return <-gift`" --> c["the caller's `gift`<br/>destroyed at the end of `Main`"]

Only a struct that is just its fields

A struct can be taken apart only when it has no destructor of its own. Give Parcel a ~Parcel, and that destructor needs the whole parcel to run on — so the same pattern is refused:

error: cannot split 'Parcel' with a moving pattern, because it declares destructor '~Parcel'
  note: '~Parcel' runs on the whole value, so no part of it can be taken out on its own
  help: bind the whole value and read its fields, or match a value that stays with its owner
You wantWriteAllowed when
one field, moved out<-parcel.giftnever
every field, each to one ownerlet Parcel { wrapping: _, gift: gift } <- parcel;the struct has no destructor
to look at a fieldparcel.gift.namealways — reading moves nothing

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A parcel is wrapping paper around a gift. It is tempting to move just the gift out and leave the
// rest where it is, but Rux refuses: a field cannot be moved out of a value on its own.
//
//     return <-parcel.gift;
//
// error: cannot move field 'gift' out of droppable value 'parcel'
//   note: partial moves would leave the aggregate with only some fields initialized
//   help: move the complete aggregate or borrow or clone the component explicitly
//
// Half a parcel would be a value that is partly alive, and the compiler could no longer say what
// to destroy when its scope ends.
//
// What works is taking the whole value apart. A struct pattern with `<-` moves the parcel into
// the pattern, and every field gets exactly one owner again: a field bound to a name belongs to
// that binding, and a field bound to `_`, or left out of the pattern, belongs to nobody and is
// destroyed on the spot. So the gift moves on and the wrapping is gone before the next line runs.
//
// Only a struct that is nothing more than its fields can be taken apart. Give `Parcel` its own
// destructor and that destructor needs the whole parcel, so the same pattern is refused:
//
// error: cannot split 'Parcel' with a moving pattern, because it declares destructor '~Parcel'
//   note: '~Parcel' runs on the whole value, so no part of it can be taken out on its own
//   help: bind the whole value and read its fields, or match a value that stays with its owner
import Io::PrintLine;

struct Item {
    name: char8[..];
}

extend Item {
    func =(self: &var Item, other: &Item);

    func ~Item(self: &var Item) {
        PrintLine("    {} destroyed", self.name);
    }
}

// No destructor of its own, so a moving pattern may take it apart.
struct Parcel {
    wrapping: Item;
    gift: Item;
}

// The gift moves out to the caller. The wrapping is bound to `_`, so it is destroyed at the `let`.
func Unwrap(parcel: Parcel) -> Item {
    let Parcel { wrapping: _, gift: gift } <- parcel;
    PrintLine("    unwrapped the {}", gift.name);
    return <-gift;
}

func Main() -> int {
    PrintLine("unwrapping:");
    let gift = Unwrap(Parcel { wrapping: Item { name: "paper" }, gift: Item { name: "book" } });
    PrintLine("    reading the {}", gift.name);

    PrintLine("end of Main:");
    return 0;
}

Run it

cd Examples/Ownership/PartialMove
rux run
unwrapping:
    paper destroyed
    unwrapped the book
    reading the book
end of Main:
    book destroyed

Common mistakes

Try it yourself

  1. Leave wrapping out of the pattern altogether: let Parcel { gift: gift } <- parcel;. Is the paper still destroyed at the let?
  2. In Main, build a parcel and take it apart into two bindings, wrapping: w, gift: g. At the end of Main, which item is destroyed first?
  3. Add func ~Parcel(self: &var Parcel) that prints "parcel opened", and read the error. Then change Unwrap so it only prints parcel.gift.name and returns nothing.

Learn more