Quick little confusion or even foot-gun I ran into (while working on the challenge I posed earlier).

TLDR

My understanding of what I ran into here:

  • Matching on multiple variables simultaneously requires assigning them to a tuple (?),
  • which happens more or less implicitly (?),
  • and which takes ownership of said variables.
  • This ownership doesn’t occur when matching against a single variable (?)
  • Depending on the variables and what’s happening in the match arms this difference can be the difference between compiling and not.

Anyone got insights they’re willing to share??

Intro

I had some logic that entailed matching on two variables. Instead of having two match statements, one nested in the other, I figured it’d be more elegant to match on both simultaneously.

An inline tuple seemed the obvious way to do so and it works, but it seems that the tuple creates some ownership problems I didn’t anticipate, mostly because I was thinking of it as essentially syntax and not an actual tuple variable.

As you’ll see below, the same logic with nested match statements each on a single variable doesn’t suffer from the same issues.

Demo Code

fn main() {

    // # Data structures
    enum Kind {
        A,
        B
    }
    struct Data {
        kind: Kind
    }

    // # Implementation
    let data = vec![Data{kind: Kind::A}];

    // ## Basic idea: process two adjacent data points
    let prev_data = data.last().unwrap();
    let new_data = Data{kind: Kind::B};

    // --- MATCH STATEMENTS ---

    // ## This works: match on one then the other
    let next_data = match prev_data.kind {
        Kind::A => match new_data.kind {
            Kind::A => 1,
            Kind::B => 2,
        },
        Kind::B => match new_data.kind {
            Kind::A => 3,
            Kind::B => 4,
        },
    };

    // ## This does NOT work: match on both
    let next_data2 = match (prev_data.kind, new_data.kind) {
        (Kind::A, Kind::A) => 1,
        (Kind::A, Kind::B) => 2,
        (Kind::B, Kind::A) => 3,
        (Kind::B, Kind::B) => 4,
    };
}

The Error

The error is on the line let next_data = match (prev_data.kind, new_data.kind), specifically the tuple and its first element prev_data.kind, with the error:

error[E0507]: cannot move out of `prev_data.kind` which is behind a shared reference

move occurs because `prev_data.kind` has type `Kind`, which does not implement the `Copy` trait

The Confusion

So prev_data.kind needs to be moved. That’s ok. Borrowing it with (&prev_data.kind, ...) fixes the problem just fine, though that can cause issues if I then want to move the variable within the match statement, which was generally the idea of the logic I was trying to write.

What got me was that the same logic but with nested match statements works just fine.

I’m still not clear on this, but it seems that the inline tuple in the second tuple-based approach is a variable that takes ownership of the variables assigned to it. Which makes perfect sense … my simple mind just thought of it as syntax for interleaving multiple match statements I suppose. In the case of nested match statements however, I’m guessing that each match statement is its own scope.

The main thing I haven’t been able to clarify is what are the ownership dynamics/behaviours of match statements?? It seems that there’s maybe a bit happening implicitly here?? I haven’t looked super hard but it does seem like something that’s readily glossed over in the materials I’ve seen thus far??

General Implications

AFAICT:

  • if you want to match on two or more variables simultaneously, you’ll probably need borrow them in the match statement if they’re anything but directly owned variables.
  • If you want to then use or move them in the match arms you may have to wrangle with ownership or just use nested match statements instead (or refactor your logic/implementation).
  • So probably don’t do multi-variable matching unless the tuple of variables is a variable native to the logic?