Beginner - &Some(p) and Some(&p)

Hello friendly people, hope I can bother you with a beginner question.

This is the third Option exercise in Rustlings. (full code at the bottom of this post) The idea is to match on a value, and then use the value again after the match. The value is moved to the match, so it does not compile because the println!() thereafter accesses the value too.

So, instead, the match should be on a reference to the value. I would think that this would be the solution:

match &optional_point {
        &Some(p) => println!("Coordinates are {},{}", p.x, p.y),
        _ => panic!("No match!"),
    }

I added a & to the Some(p) because I expected &optional_point to be &Some(p) when optional_point is Some(p). But that gives a compiler error 'cannot move out of a shared reference': p is moved into the &Some(p). My first question is: what does that mean? It has nothing to do with the print statement afterwards, because if that is commented out the error stays the same. And why is this not an issue in the second (correct) solution below?

After trial and error I found the solution is

match &optional_point {
        Some(p) => println!("Coordinates are {},{}", p.x, p.y),
        _ => panic!("No match!"),
    }

My other questions are: how can &optional_point match on Some(p) without &? Is there automatic dereferencing going on? (not a big fan of that, as it feels I can't get a grip on what is happening). In VSCode the types of parameters are automatically shown. In the first, incorrect, solution, the type of p is Point, in the second, correct, solution the type of p is &Point. I don´t understand how the type of p can have become &Point, when the match is not on a reference. How does that work?

I apologize for the frustration that is probably noticable. The mechanics of borrowing, Option, match etc. are actually clear. But somehow, I can't seem to predict how stuff interacts and this is one of those instances. I mean, it is easy to solve by just following compiler's orders in adding or removing ampersands, but I would love if I could actually predict those orders myself, from the principles of borrowing.

Full code of the exercise:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let optional_point = Some(Point { x: 100, y: 200 });

    // TODO: Fix the compiler error by adding something to this match statement.
    match optional_point {
        Some(p) => println!("Coordinates are {},{}", p.x, p.y),
        _ => panic!("No match!"),
    }

    println!("{optional_point:?}"); // Don't change this line.
}

When you do let p = point_variable, you move the point_variable into the p variable, and you can't use the point_variable anymore. (See the description of move semantics and copy semantics here, if you're unaware of them.) And if you try something like this:

let r = &point_variable;
let p = *r;

You'll get an error about trying to move a value out from behind a reference. If you were allowed to do that, r would be pointing at uninitialized memory,[1] which would be bad.

And this attempts to do the same thing:

let r = &point_variable;
// let p = *r;
let &p = r;

Which is getting us pretty close to your error. I'll come back to that in a moment.

Yes, there's something called "default binding modes"[2] which allows variables introduced in the pattern ("bindings") to be a reference to the place it lines up with. When you matched the non-reference Some(p) against the reference type &Option<_>, that made your binding mode be by-reference instead of by-move. (I'll also say more about binding modes in a moment.)

Before edition 2024, how default binding modes worked was quite the mess, and the behavior wasn't even fully documented. They became simpler in edition 2024, but I'm not surprised you still find the magic behavior confusing.

I think patterns are best understood if we present how they work in layers.

Understand that patterns are duals

That's my favorite introduction to the concept of patterns. It was written before the default binding modes feature, and in fact doesn't talk about non-move bindings at all. But after reading the article, you should be able to understand why these attempt to do the same thing:

let p = *r;
let &p = r;

And what was going on with your error here:

match &optional_point {
        &Some(p) => println!("Coordinates are {},{}", p.x, p.y),

The & in &optional_point got "peeled off" by the & in the &Some(p) pattern, and you tried to move the value into p. But you can't move from behind references, so you got an error.

Learn about binding modes

I've already hinted that default binding modes didn't use to exist. So how can you fix that last code snippet without them?

You would do this:[3]

match optional_point {
        //   vvv new
        Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),

By adding the ref modifier before p, this now creates a reference to the value in-place and assigns the reference to p instead of trying to move the value into p. If you wanted a &mut _ instead of a &_, you would use ref mut p instead.

These binding modifiers allow you to make references to places somehow nested within a value. In this case, we had an Option<Point> not an Option<&Point>, but were still able to take a reference to the Point (when it exists).

Get the hang of default binding modes

I linked the official documentation above, but the general gist is that when you match a reference type against a non-reference pattern, all your bindings (new variables) implicitly have the ref (or ref mut) behavior. So another way to write the last code block is:

    match &optional_point {
        Some(p) => println!("Coordinates are {},{}", p.x, p.y),

And we're back to your code that worked.

Default binding modes make some things simpler to write, so you don't have to sprinkle ref mut everywhere or such...

if let Something { complicated, With(multiple_variables) } = some_ref_mut { ... }

...but because the non-binding parts of the pattern no longer exactly matches up with the types of the value, it can add confusion in complicated cases. (You'll probably get use to simple cases pretty quick.)

If you want to make sure some code isn't using default binding modes so more easily be sure what's going on, you can add #![warn(clippy::pattern_type_mismatch)][4] to the file and run Clippy to see where default binding modes are taking effect. (You can run Clippy in the playground under Tools in the top right.)


  1. with move semantics, you can considered the moved-from place to be uninitialized ↩︎

  2. also called "match ergonomics", though at least historically not everyone has agreed on exactly how ergonomic they actually are ↩︎

  3. You could also add & to both optional_point and Some(ref p), but since the pattern immediately peels off the & when you do that, there's no point in doing so. ↩︎

  4. or deny ↩︎

9 Likes

I am deeply impressed and grateful for the fast, detailed, empathic, and full of references answer. Although I studied your message and some of its references, I can't quite put into words yet what my follow-up question will be. But I did not want to wait with expressing my gratitude for this effort :folded_hands:

1 Like

Not much to add to the usual amazing response from @quinedot; but if you follow their suggestion to warn/deny/forbid clippy::pattern_type_mismatch, you may want to ensure that you allow/expect clippy::needless_borrowed_reference as well since that lint is warn-by-default and will conflict with clippy::pattern_type_mismatch[1].


  1. You may want to also ensure that you allow/expect clippy::ref_patterns as well; however that is allow-by-default; so that should only be necessary if you have deny/warn/forbid it. ↩︎

2 Likes

Question since the "duals" post you linked doesn't have an example, presumably you'd pattern match on &None as well since the "shape" of &Option<T> would seem to require it?

match &optional_point {
    &Some(p) => println!("Coordinates are {},{}", p.x, p.y),
    &None => {}
}

It definitely looks weird to have &None, but the "shape" of &Option<T> would seem to mandate it. Perhaps that is why I usually write something like:

match *&optional_point {
    Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
    None => {}
}

because it seems more "consistent" without the "weirdness" of &None.

Before default binding modes, you would need &None to compile (without the & you get a type mismatch error). But really you just wouldn't add & to the scrutinee and & to beginning of every pattern. (Relatedly, there's no need to add *& to the scrutinee.)

Now using None with &optional_point would just change the binding mode (even though nothing gets bound in that arm).

Indeed. I should have actually tried to compile the code using an older version of rustc before inquiring.

Yeah, I was operating on the assumption that the OP wanted to avoid default binding modes; in which case, I was offering an alternative to[1]

match &optional_point {
    &Some(p) => println!("Coordinates are {},{}", p.x, p.y),
    &None => {}
}

OP, you can experiment with older versions of Rust; while the language has evolved greatly since its inception, I personally find that learning the history of things can help illuminate why/how we got to where we are even in technical fields like math. Some do disagree, however, and may tell you that experimenting with older versions of Rust may be harmful to your development. If you were to use an older version of the compiler (e.g., installing it via rustup install 1.0.0), you should avoid "match ergonomics" even though I'd argue it's the more idiomatic way nowadays. There are cases where code won't even compile without it.


  1. I was also assuming &optional_point was shorthand for a variable that was already a &Option<T>. I would certainly not take a reference just to dereference it. In the case where I have an Option<T>; then yes I'd have match optional_point { Some(ref p) => {}, None => {} }. ↩︎