Why "nested" matches work different from "tupled" matches?

Hi everyone!

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:

struct Cpu {  ld: Load,  }
enum Load {
  Idle(i32),
  Busy(i32),
}

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”:

fn add2(t: &mut Cpu, u: &Cpu) {
match (t.ld, u.ld) {
    (Load::Idle(i1), Load::Idle(i2)) => t.ld = Load::Idle(i1+i2),
    // pair for Busy

But rust complained about “cannot move out of borrowed content”. After reading the forum I found two ways to fix the error:

  1. add #[derive(Clone,Copy)] to both struct and enum

  2. The first way does not work if struct has non-copyable fields like String, so it can be fixed with:

    match (&t.ld, &u.ld) {
    (Load::Idle(ref i1), Load::Idle(ref i2)) => t.ld = Load::Idle(i1 + i2),

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.

Please see the full code at https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=1ab3b88265b8191c3ce3bae9be656616 . It is compilable and it works - using the “nested” way. A function add2 that uses “tupled” matches is commented out.

Thank you!

Note that in the 2018 edition, the following works:

fn add2(t: &mut Cpu, u: &Cpu) {
    match (&t.ld, &u.ld) {
        (Load::Idle(i1), Load::Idle(i2)) => t.ld = Load::Idle(i1+i2),
        (Load::Busy(i1), Load::Busy(i2)) => t.ld = Load::Busy(i1+i2),
        _ => {},
    }
}

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.

To share some terminology:

match t.ld { ... }

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.

1 Like

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.