Idea: `move x`, in analogue with `ref x`

I brought this up on Discord and it seems well-received enough that I’d like to get some opinions on it.

I propose introducing a new production to the pattern grammar: move $pat, with the following semantics:

match &Foo(0) {
    Foo(x) => { // secretly Foo(ref x)
        // x: &i32, created by the usual projection
    }
}

match &Foo(0) {
    Foo(move x) => { 
        // x: i32, created by deref (note that i32: Copy, so this 
        // isn't the *best* example)
    }
}

Formally, it acts as a dual to ref; before match ergonomics, this “move” pattern context was the default, but this is now dependent on the matched expression’s type. I’m proposing this pattern to be able to inform the compiler which mode you’d like, rather than allowing it to be inferred as ref by mistake. While I’ve been told that "ref is on the way out", I think having explicit restructuring modes is valuable, even if they’re rarely used (the only use of move is move-into closures, which are similarly rare).

I actually didn’t think of this because of match ergonomics, but from a discussion on how rustc should unify incomplete types in ascribed patterns. I’d like to hear whether this would solve any of the common complains about match ergonomics (I haven’t had time to come up with too many examples).

6 Likes

This actually would have saved me a bit of time a few days ago.

I had a match where the whole thing was behind a reference and the correct solution to my compile error was to put a & in front of the whole pattern, such that the inner binding was by-move. (IIRC it was a copy type, and one that was being used multiple times so dereferencing at the use site was ugly.)

However, what I first attempted to do was to stick a & in front of the binding to de-ref it (which would work if it were a reference in pre-binding-modes). This did not work and threw a different error that did not help me.

I think the most common point of complaint against match ergonomics is in something like let Struct { a, b } = make_struct;, however; would a move unref be allowed before these bindings to enforce that these are by-move?

1 Like

This has come up a few times, e.g. on the match ergonomics tracking issue. A similar feature would be to permit & patterns on non-reference values when in ref binding mode, aligning with the existing behavior of & patterns. This is something existing users tend to reach for intuitively- @Boscop, @nikomatsakis, and myself in the tracking issue, @CAD97 here, etc.

And as I mentioned at the end of that thread, we may actually want both of these. move seems to fit the mental model implied by match ergonomics (“a &T is just a T with fewer permissions”), while & fits the mental model implied by older match behavior (“a &T is a pointer to a T" or alternatively "& is part of the structure of a type”).

We seem to want to support both of these mental models in several places, not just match ergonomics, as they both make sense in various contexts.

3 Likes

I'm not surprised this isn't a novel idea!

Same semantics? That seems like a curious way to spell it, and not what I'd have reached for... I think that the semantics of ref and & are, to this day, a bit muddy in my head.

I'm curious what those places are. I'd never quite thought of "&T is a weaker T", but I suppose it makes sense in the scenario of "I pick this type apart with restructuring".

Was this by someone in an official position?

Yes, same semantics. To explain why, you can think of &T in much the same way as you would think of Box<T> or even just some struct Foo(T)- as a "containing" type that contributes to a value's structure.

Construction and pattern matching syntax match, so just as you construct a Foo via Foo(some_t) and destructure it via Foo(some_t), you construct a &T via &some_t and destructure it via &some_t. On both sides, some_t is a T.

This can be confusing if you've not internalized it, which is part of the motivation for match ergonomics as well as statements like "ref is on the way out." (And yes, @phaylon, this has been said by people on the lang team, as an expression of the intent to find something better.)

Autoderef and autoref apply this logic to method call syntax. For what it's worth, nikomatsakis is usually the one I hear this from.

Well, I do hope the same capabilities will still be available.

I believe @mcy was paraphrasing me from our Discord conversation. I want to clarify that what I meant by this is that I believe ref will be used much less frequently given match ergonomics. I have no plans to propose deprecating ref and I think I would be opposed to it at this stage.


Regarding the idea of having move, it seems like filling a missing piece of the puzzle in a natural way. In particular, given type ascription, you can say:

match expr {
    Some(move x: &usize) => { println!("{:?}", x); }
}

and you will know that expr : &Option<&usize> and that expr : &Option<usize> is not possible.

Furthermore, consider:

trait Foo { 
    fn new() -> Self;
}

impl Foo for &'static Option<usize> {
    fn new() -> Self { &Some(0) }
}

impl Foo for &'static Option<&'static usize> {
    fn new() -> Self { &Some(&1) }
}

fn main() {
    let x: &_ = Foo::new();
    if let Some(x) = x {
        let x: &usize = x;
        // Prints 0.
        println!("{}", x);
    }
}

In this case, the dynamic semantics of printing x is directly affected by which mode you pick. By having move, we could ensure that 1 gets printed.

Having move also strikes me as advantageous for teaching by showing an in-language desugaring for match ergonomics.

All in all, I believe move $pat is an idea worth considering.

Excellent, thanks. Just wanted to know if I should watch out for RFCs.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.