Let's suppose I want to monitor CPU load and collect how much time it is idle(0-10% load) or very busy(90-100% load). And I have some a watcher that returns me a value with enum:
Next step I want to create a function that can add two Cpu variables: it must increase the first variable only if both variable have the same enum type. The first idea worked but looked ugly(excerpt - please see the full version on playground) - nested matches:
fn add(t: &mut Cpu, u: &Cpu) {
match t.ld {
Load::Idle(i1) => match u.ld {
Load::Idle(i2) => t.ld = Load::Idle(i1+i2),
_ => {},
}, // the same for Busy pair
I remembered about the power of match and tried to make it "tupled":
The latter way does not look nice, too. Though it works and does not clone or copy anything.
The question is: why "nested" matches works as is, but "tupled" ones require workarounds? It is so confusing at first time, as by intuition, the naive "tupled" way is expected to work after one sees that "nested" way works fine.
The driver behind this -- non-lexical lifetimes (NLL) -- is enabled for the 2018 edition and will be eventually coming to the 2015 edition as well.
As for why the original version works: this is because the match is still by-move (if you put a & on it, the 2015 borrowck rejects it). But because the only captured values are Copy, this doesn't invalidate the place you're moving from.
Basically, you're doing a partial move, and the partial move is actually a partial copy. This version won't work if the attached data isn't Copy, either.
When you pack it into a tuple before matching, the full wrapping enum is moved into the tuple. Because it's not Copy, this tries to invalidate the place it came from, which is illegal, as it's behind a borrow.
Borrowing is the answer here, matching on (&t.ld, &u.ld). This avoids moving the non-move containers, and allows you to manipulate references until you need to copy out and in data for the update.
t.ld here is a form of expression known as a place expression. These are limited to foo, (foo), foo.field, *foo, foo[index], and any combination thereof (e.g. foo[2].bar[i][j].field).
Place expressions are comparable to the concept of "lvalues" in C++, and when they appear directly as the argument to match or if let PAT =, the compiler does not consider them to be moved; just like how they are not considered to be moved when they appear on the left-hand side of an assignment (foo.field = 3;) or when you borrow them (&foo.field).
move (t.ld, u.ld) { ... }
// or similarly
match { t.ld } { ... }
Here, { t.ld } and (t.ld, u.ld) are value expressions (comparable to rvalues in C++). These expressions are evaluated to produce a new value before the match is performed, and that evaluation causes the ld members to be moved.
These concepts are not usually discussed in introductory materials to rust, but they are documented in the WIP Rust Reference, which seems to be coming along nicely.
Thank you for detailed explanations and extra info about coming Rust 2018 feature. It gets clearer.
My first guess was that match was not able to work with more than one variable and it had to create a temporary one. So, match (a.n, b.n) {... was a syntax sugar for let tmp = (a.n, b.n); match tmp {... But I decided to ask to learn what really was going on.