Ownership, and why signatures look like that
Every value in Rust has exactly one owner. When the owner goes out of scope the value is dropped, and no garbage collector is involved. That single rule is why Rust signatures carry more punctuation than you may be used to: a signature has to say whether it is taking a value, borrowing it, or borrowing it to change it.
Three forms, and you can read them off almost any function here:
fn takes(m: Manifest) // takes ownership; the caller no longer has itfn borrows(m: &Manifest) // reads it; the caller keeps itfn changes(m: &mut Manifest) // may modify it; nobody else may touch it meanwhileThe rule the compiler enforces is that you may have either any number of &
borrows or exactly one &mut, never both. That same rule is what makes data
races impossible, and it comes back in Guards, Drop and
!Send as the reason you can make a lock unforgettable.
Reading a real one
Section titled “Reading a real one”index::ingest::index_one is the middle of the ingest pipeline:
pub(in crate::index) fn index_one( cx: &Ingest, source: &Source, search: &Mutex<SearchIndex>, name: Option<&str>, collection: (&CollectionId, &str),) -> Result<record::Indexed>Every parameter is a borrow, so this function reads all of them and takes none of
them away from the caller. search is a &Mutex<…>, a shared borrow of
something you can still mutate. That escape hatch comes up in
Concurrency.
The return type used to be different, and the reason it changed is a lifetime story.
Lifetimes, and why Indexed is owned
Section titled “Lifetimes, and why Indexed is owned”A borrow has to be backed by a value that outlives it. When a struct holds
borrowed data, it needs a lifetime parameter saying so. index::ingest::Ingest
is one:
pub struct Ingest<'a> { home: &'a Path, name: Option<&'a str>, // …}Read 'a as “some scope”; the struct is promising not to outlive whatever it
borrowed. You never pick the scope yourself, the compiler does, and most of the
time you only notice lifetimes when one is too short.
record::Indexed (everything the pipeline learned about one WACZ) used to look
the same way, holding &str and &Source pointing into index_one’s own
variables. That was fine while index_one applied it to the manifest itself. When
the locking work changed the design so that index_one returns the value and
its caller decides when to write it, those borrows stopped being possible: the
value now outlives the function that built it.
So Indexed became owned: String instead of &str, Source instead of
&Source. That costs a handful of clones per crawl, nothing next to reading a
WACZ, and it bought the ability to hold the value across a decision. Most
lifetime problems have that shape. They are a question about how long something
needs to live, and cloning is a respectable answer.
Clone is not a failure
Section titled “Clone is not a failure”Coming from a GC language it is easy to read .clone() as something you got
wrong. Sometimes it is, in a hot loop. Usually it is the cheapest way to stop
threading a lifetime through six signatures to save an allocation that does not
matter. indice clones freely at the edges and is careful in index::ingest::pages,
where the per-record work is hot.
What to take forward
Section titled “What to take forward”- A
&in a signature means the caller keeps the value. - A
&mutmeans exclusive access for the duration. - A
'ameans the struct is holding a borrow and cannot outlive it. - If a lifetime is fighting you, ask how long the value needs to live. Owning it is often the answer.