cross-posted from: https://lemmy.ml/post/13353225
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 elementprev_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?
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, 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.