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?
  • maegul (he/they)@lemmy.mlOPM
    link
    fedilink
    English
    arrow-up
    2
    ·
    edit-2
    8 months ago

    Actually The Book does cover this topic in chapter 6.2 (a rather obvious place that I should have found). See How matches interact with ownership.

    It mentions the idiom of matching on a reference rather than a plain variable for when some action with the contents of a enum is desired without moving it.

    More relevant to my sense that some implicit mechanism is in play, the last paragraph touches on “Pushing Down” a reference and binding modes:

    Rust will “push down” the reference from the outer enum, &Option<String>, to the inner field, &String. Therefore s has type &String, and opt can be used after the match. To better understand this “pushing down” mechanism, see the section about binding modes in the Rust Reference.

    It seems like fairly intuitive behaviour, but still, once I was getting compiler errors I could tell implicit things I didn’t understand were happening.

    Also I don’t see anywhere in this chapter the suggestion that one should do what I was trying to do by matching on two variables simultaneously … so I’m guessing I was just doing something that’s naive if not stupid.

  • nmtake@lemm.ee
    link
    fedilink
    English
    arrow-up
    2
    ·
    8 months ago

    If I understood correctly, the first match expression doesn’t take the ownership of the prev_data.kind because the prev_data.kind is a place expression:

    https://doc.rust-lang.org/stable/reference/expressions.html#place-expressions-and-value-expressions

    A place expression is an expression that represents a memory location.

    https://doc.rust-lang.org/stable/reference/expressions/match-expr.html#match-expressions

    When the scrutinee expression is a place expression, the match does not allocate a temporary location; however, a by-value binding may copy or move from the memory location.

    I’m not sure what “a by-value binding may copy or move from the memory location” does mean, but I beleive no allocation means no move.

    For the second match, move happens. The tuple (prev_data.kind, new_data.kind) tries to take an ownership of the prev_data.kind, but the prev_data is &Data (borrowed from the vec data), so the tuple can’t take the ownership.

    • maegul (he/they)@lemmy.mlOPM
      link
      fedilink
      English
      arrow-up
      2
      ·
      8 months ago

      Thanks!!!

      Yea this matches (heh) where I got up to, including that there’s some “smart” stuff happening under the hood regarding how exactly the “scrutinee” (variable being matched on) is passed into the March statement.

      But I was not aware of the place/value expression distinction, terminologically at least, and those links are very helpful (I’d looked into the reference a bit but it felt like a rabbit hole).

      Additionally, I think this line from your second link is relevant:

      any variables bound by the pattern are assigned to local variables in the arm’s block, and control enters the block

      So whenever you’re using a tuple as I showed above, I’d guess that it’s the tuple that enters the arm’s block and not any bound variables directly, which disturbs the ownership flow.