Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit move binding mode #3410

Open
wants to merge 35 commits into
base: master
Choose a base branch
from

Conversation

schuelermine
Copy link

@schuelermine schuelermine commented Apr 7, 2023

This RFC suggests using the move keyword to explicitly specify the moving binding mode to override match ergonomics

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 8, 2023
@steffahn
Copy link
Member

steffahn commented Apr 8, 2023

For context, there’s a URLO discussion where the motivation for writing this RFC probably emerged. I don’t know yet what I’d like to contribute from that thread to this discussion… but to paraphrase my own take on binding modes in the context of match ergonomics from that thread:

I believe it might be worth a change to lint (or eventually even error, or error-by-default-lint) in general against any ref x, ref mut x or mut x that appears in places where match ergonomics have changed the default binding mode. I think the current behavior has confusion points such as

  • ref x and x, or ref mut x and x, are identical in meaning in match-ergonomics contexts, and different syntax for the same thing can lead to confusion, in particular when their meaning differs significantly in other contexts.
  • the current state of mut x and x (in the context of match ergonomics) differing in whether or not binding-by-reference happens is just obviously confusing and nothing else

In case that isn’t clear from the above, I believe that complicated settings should require the user to fall back to patterns that properly match the type, i.e. to no longer use match ergonomics. This could also be aided by help messages in diagnostics.


I don’t want to say that there’s no truth to the argument that “requiring the user to add lots of ref” to convert from match ergonomics to classical patterns, just in order to change one binding mode, might have some significant inconvenience in some use-cases. But the current state of binding-mode specifiers interacting with match-ergonomic’s default binding modes seems so messy and inconsistent to me that patching it up by adding even more syntax seems like the wrong approach. That being said, I would like the idea of exploring how reasonable the thing listed under “alternatives” could be, an idea that I described in the URLO thread with the words

let (PAT1, PAT2) = r; with r: &(S, T) will create a &S and match it against PAT1 ; as well as creating a &T and matching it against PAT2

there’d be some consistency to that; and the way to write move x from this thread under that approach would be simply &x, as opposed to x which would (already) bind x to a reference. Naturally such a change would need disallowing any binding mode specifiers in places with changed default binding mode as a prerequisite. Following such an idea, a place with (what is now) a bind-by-reference default binding mode context would (if we re-allowed binding modes in such places at all) treat ref x as opposed to x by binding x to yet another level of (reference) indirection; which would probably not be particularly useful, but at least consistent (and different from what it does now); and mut x as opposed to x would still bind by-reference, but the variable x holding the reference would become mutable.

@schuelermine
Copy link
Author

You are correct that this RFC originates from that discussion. I originally included a link to it but was advised against it by a member of the Discord community. I will update the alternatives section to elaborate further on your alternative.

@VitWW
Copy link

VitWW commented Apr 8, 2023

As an alternative: Rust use *mut and *const for raw pointers. So it is handy to reuse this keyword here

let (x, const y, mut z) = &mut xyz;
// The type of `x` is `&mut i32` and
// the type of `y` is `&i32` and
// the type of `z` is `i32` (and the binding is mutable)

let (x, const y) = &xy;
// The type of `x` is `&i32` and
// the type of `y` is `i32`

P.S. It is legal already to write

let (x, mut y) = &xy;
let (x, ref y, mut z) = &mut xyz;

@VitWW
Copy link

VitWW commented Apr 9, 2023

For context, an alternative to ref/move

(ref | move)? mut? IDENTIFIER (@ PatternNoTopAlt)?

is mut/const

ref? (const| mut)? IDENTIFIER (@ PatternNoTopAlt)?

or both

@H2CO3
Copy link

H2CO3 commented Apr 10, 2023

There is no need for anything like this. It's just piling even more special cases on top of the already-confusing match "ergonomics". Just make sure that the pattern's type matches that of the value, it's as simple as that.

@schuelermine
Copy link
Author

This isn’t a special case. It’s simply filling an obvious gap in the existing match ergonomics. If you don’t like match ergonomics, that’s fine, but it’s here and people use it.

Even if the “better” approach is to just not use match ergonomics, having a half-baked feature in the language is worse than having one that’s on par with the alternative in what it can do.

Also, have you read the alternatives section of the RFC? It contains an alternative implementation of match ergonomics suggested by @steffahn that may be more to your liking.

@schuelermine
Copy link
Author

schuelermine commented Apr 14, 2023

If ref is unreadable, noisy, and tedious to write, then why isn't the same true for move (which is longer)? This argument is inconsistent.

I’m not suggesting putting move everywhere. What I’m suggesting is reducing noise by setting the default binding mode to the most common one and explicitly specifying the binding sites that deviate from this.

@schuelermine
Copy link
Author

That said, I like @steffahn 's suggestion (the alternative you listed). I have often wondered why match ergonomics doesn't already work this way. Is there a reason beyond "nobody implemented it"?

I have talked to other community members on the community Discord Server about this and there seems to be no clear answer. Perhaps no-one came up with this during the match ergonomics RFC period. However there may be subtle problems in the implementation which is why I didn’t feel confident to suggest this change. I feel this proposal is much more conservative as it suggest exposing functionality that is already present (mut’s effect on match ergonomics) and does not require an edition boundary.

@schuelermine
Copy link
Author

I have seen plenty of such code in the wild. Most often it's a codebase which was started before match ergonomics were implemented. Sometimes it's a preference of the people writing it. In those cases match ergonomics aren't used, and the matching is done old-style, with a dereference on the scrutinee and explicit ref/ref mut bindings.

I was talking about unnecessary ref or ref mut, when match ergonomics already set them as the default. Have you seen that?

@afetisov
Copy link

Well, the point of match ergonomics is to avoid writing ref/ref mut. So no, I haven't seen those used together.

Please don't spread your replies over many messages, it makes hard to keep track of discussion.

@steffahn
Copy link
Member

I kind of like the move keyword option a bit more than the &x option, in that &x is known to be confusing, but I am a bit worried about the choice of move keyword here.

Regardless of choice of keyword (let's go with move for the sake of the argument), if people learn that move x can be used in certain contexts to turn what used to be a reference x: &i32, without the word move, into an owned x: i32, there will IMO inevitably be confusion as to why that doesn't work consistently everywhere. It will intuitively function like a dereferencing pattern, because the thing that it intuitively does is dereferencing - the fact that the "true" functionality would be to affect the "default binding mode" is a language design detail that most people will be unaware of, or at least not know in full detail.

Of course, assuming this outcome is realistic, it would be a bad outcome, because we already have true dereferencing patterns &x and &mut x, so if the common perception would be that move x is effectively a third kind of dereferencing pattern that must be used in certain cases, whereas it cannot be used in others, that seems to me like bad language design and unnecessary complication. E. g. iterating over HashMap<i32, String>, it you want k: i32 and v: &String, you would need to write .iter().for_each(|(&k, v)|) but for Vec<(i32, String)> it would be .iter().for_each(|(move k, v)|) – a differentiation in contrast to the case of a simple (k, v) pattern, which can be used for both consistently and with the consistent outcome k: &i32 and v: &String.

If we follow argument that &x is more confusing than something like move x, which may very well be true, as reference patterns are known to be a slightly hard thing to learn due to the dual nature of patterns, then someone might even come to prefer move x and/or learn about it first, and then wonder why they cannot simply write .iter().for_each(|(move k, v)|) in the HashMap<i32, String> case - or possibly even - why they can still write it, but with vastly different outcome (as move k would change nothing compared to k).


I feel this proposal is much more conservative as it suggest exposing functionality that is already present (mut’s effect on match ergonomics) and does not require an edition boundary.

Regarding edition boundaries... the kind of syntax that might change in meaning over an edition is mut x and ref x/ref mut x in by-reference default binding mode contexts. These are of limited usefulness anyways, their new respective meanings would be to create a reference held in a mutable variable, and to create a reference to a reference. Both are operations that used to be completely impossible to achieve anyways with normal patterns and binding modes before any match ergonomics, so they cannot be all that important.

Without/before an edition boundary, we can simply lint against all this syntax that would change in meaning (because it would then become a corner case incompatible with the new concept of how match ergonomics operate, only kept for backwards compatibility). Even after an edition we could still consider just having it error instead of introducing the new consistent meaning of mut x/ref x/ref mut x in these contexts. If there's not much of a practical need to ever bind to references in mutable variables or references-to-references, that would not be much of a restriction.

@VitWW
Copy link

VitWW commented Apr 15, 2023

@steffahn
That's why const (not mut) is an alternative to move/ value(not ref). It is already in use for raw types, and the content of immutability is clear

@schuelermine
Copy link
Author

@VitWW
I don’t think const is a good keyword, since it usually implies knowledge about the value at compile time. Raw pointers are the exception to this, not the rule. Also, it highlights that the value is immutable, as opposed to mut, but mut’s behaviour here is already confusing, and we’d rather highlight that mut and move don’t bind by reference but by value.

@stepancheg
Copy link

May I leave my two cents?

I think Rust should go in the opposite direction. Rely solely on match ergonomics and deprecate the ref keyword.

In my experience, I manage to never use the ref keyword (almost nobody in our team/project uses it). Almost all code can be written without ref in match, and exceptions are so rare that the language simplification outweighs these rare inconveniences.

@H2CO3
Copy link

H2CO3 commented Apr 20, 2023

Rely solely on match ergonomics and deprecate the ref keyword.

That would be massively harmful, primarily in unsafe code. Only having an option where types do not match is unacceptable in any language seeking correctness to any degree.

@CryZe
Copy link

CryZe commented Apr 20, 2023

@H2CO3 Can you elaborate on that further? With this RFC match ergonomics are almost identical in power to "Rust 2015 matching", meaning that Rust 2015 matching can almost be deprecated. I don't know how any of this has anything to do with unsafe or types that don't match.

@H2CO3
Copy link

H2CO3 commented Apr 20, 2023

With this RFC match ergonomics are almost identical in power to "Rust 2015 matching"

First, I don't care about "power"; it's not what I am talking about. I am talking about situations where whether or not a type is a reference is important, and you want to be explicit about types rather than letting the compiler second-guess you. In unsafe, this can cause problems (read: even UB). Here's a trivial demonstration where the call is intended to overwrite x = 137 with the value 42 but fails to, due to match ergonomics. The equivalent with explicit patterns works as intended. This is "only" a logic bug, but if the value of x were relied on by unsafe code, it can trivially lead to UB. E.g. you can rewrite the above example with 0 and a non-zero value instead, which then gets fed into NonZeroU8::new_unchecked(). Here it is.

meaning that Rust 2015 matching can almost be deprecated

There's no need to deprecate anything. Just because there's a convenience feature, it doesn't mean that the option of being explicit should be taken away from people. This is exactly the kind of slippery slope I and many others had been worrying and warning about when match ergonomics was introduced.

@stepancheg
Copy link

@H2CO3 you example show how match ergonomics might be dangerous in unsafe context. OK.

How ref would make code safe? You didn't use ref in your examples.

There's no need to deprecate anything.

I explained the reasons: ref is needed rarely, code can be written without it, and we do not need to have two ways to write the same code. Maybe these are not compelling reasons, I don't know.

If you are arguing that match ergonomics need to be deprecated instead, this is a different topic.

@H2CO3
Copy link

H2CO3 commented Apr 20, 2023

If you are arguing that match ergonomics need to be deprecated instead

I was not; refrain from putting words into my mouth.

How ref would make code safe? You didn't use ref in your examples.

Being explicit about the types would make the code safe. That may or may not involve ref.

@stepancheg
Copy link

@H2CO3

I was not; refrain from putting words into my mouth.

I'm sorry Árpád, I didn't mean to. Just trying to understand.

Being explicit about the types would make the code safe. That may or may not involve ref

I generally agree about type safety: that's what we love Rust for. I didn't get how ref makes code safer?

@schuelermine
Copy link
Author

@H2CO3
I believe I now understand what your concern is. In your example, the programmer confuses the &mut (usize, usize) with a (usize, usize), but this isn’t an error because match ergonomics kick in. Am I understanding that correctly?

@schuelermine
Copy link
Author

schuelermine commented Apr 20, 2023

I think your example also shows how as together with _ can be dangerous due to it allowing both usize and &mut usize to be coerced to pointers in different ways. If a method was used for each you would’ve gotten an error due to the wrong type. However, I think that if you wanted to be “explicit with types” you could’ve just written as *mut () explicitly and you would’ve gotten an error.

I don’t see a strong argument against move here. As long as current match ergonomics is around, there will always be the obvious discrepancy that the binding mode can be changed, but only one way, for no good reason.

@phaylon
Copy link

phaylon commented Apr 21, 2023

@stepancheg I wouldn't have fought painfully for years (and I mean literally years) to get a clippy lint accepted to enforce explicit binding modes if it were useless. I wouldn't have put myself through all of that pain, the attacks, and the insults if I wouldn't get a value out of it,.

So I would appreciate it if everyone started with the assumption that if they don't see a use for something, that doesn't mean nobody else does. And that you don't have to be convinced of it's usefulness for it to be of great value to others.

It has explicit, mechanical functions that no other parts of the language provide:

  • It allows to reduce the adaptiveness of code to changes, as @H2CO3 demonstrated. If I write a bit of unsafe code, I want to be very explicit and the compiler to tell me to make a decision when something in a used API changes in certain ways. Having the whole language be optimized to "adapt to the situation" is great, except when that change hides a semantic change. Ownership differences are semantic. &T, vs &mut T vs T behave differently, resolve differently, have different types. Lumping them all together syntactically works in the general case, but you still have to account for the one that isn't general.
  • It allows separating shared and mutable reference bindings in a single pattern. That means match arms don't need extra blocks to get rid of mutability. You know from the pattern signature what will be mutated and what won't. You can't mutate the wrong thing accidentally. Properties like that are why I like Rust. And remember, this is also available in macros where there might not be another chance to change the bindings. Having the correctness enforcing mechanics directly in the pattern syntax enables it in more places.
  • With the proposed removal, something working for owned local bindings let (x, mut y) suddenly is no longer available for references to other things? Even though shared/exclusive borrowing is one of the most fundamental parts of Rust, so is pattern matching.

So after years of discussions, roadblocks, fighting about this and all other things. I'm sorry I'm not convinced we need to remove it because not everyone uses it. There are still no suitable replacements for the things it does.

If people want to "fix" match ergonomics, I'd propose not going for just 90% of cases and have the rest not work, and not going for a hard-to-one-side total removal that leaves parts of the community in the dust. Instead go for a holistic approach where the explicit underlying mechanics give explicit control, and have a high level approach for those that don't care and where it doesn't matter much.

@stepancheg
Copy link

@phaylon I should choose my words more carefully.

I'll rephrase. I read code by various people, including open source project, and I see that ref keyword is used not very often, and and in most of these places code can be easily rewritten with match ergonomics without hurting code quality.

Even if some people find ref useful, I argue, that the language is better it there's fewer ways to write the same code (if it does not make code more verbose).

Basically, simple language is better if it is equally expressible.

It allows to reduce the adaptiveness of code to changes, as @H2CO3 demonstrated

Did he? In his examples, ref keyword was not used.

With the proposed removal, something working for owned local bindings let (x, mut y) suddenly is no longer available for references to other things?

I didn't get that. I'm only talking about ref, not mut. Maybe you could provide specific example where ref is critical?

@VitWW
Copy link

VitWW commented Apr 21, 2023

With the proposed removal, something working for owned local bindings let (x, mut y) suddenly is no longer available for references to other things?

@phaylon Just for clarity: this RFC do not propose anything to remove.
It propose add new kind of binding for "not ref" - move/value , for example let (value x, value mut y) = v

@H2CO3
Copy link

H2CO3 commented Apr 21, 2023

Did he? In his examples, ref keyword was not used.

You keep coming back to whether ref was used or not. But it doesn't matter, it's not the point. The point is that explicitness with the type helps.

If you want, you can trivially rewrite my earlier example to include ref by pretending that you want the 2nd element of the tuple to be a reference, and now you have to use ref in the second version. Here is the match ergonomics version, and here is the explicit one. But this is a red herring, it doesn't change either the problem or the solution in any meaningful way.

@schuelermine
Copy link
Author

@phaylon

It allows separating shared and mutable reference bindings in a single pattern. That means match arms don't need extra blocks to get rid of mutability.

You can do this using match ergonomics, as they currently exist, too—you just have to explicitly specify ref if the default binding mode is ref mut. In the spirit of this adaptability, my proposal is to allow switching to a move/copy, too.

@phaylon
Copy link

phaylon commented Apr 21, 2023

@stepancheg

Even if some people find ref useful, I argue, that the language is better it there's fewer ways to write the same code (if it does not make code more verbose).

Basically, simple language is better if it is equally expressible.

But it isn't equally expressible. It can't express the restrictions and it can't express multi-mode destructuring.

The restrictions are of the same value as all other restrictions in Rust. See:

object.method();
// vs
Object::method(&object);

If someone removes Object::method the first one will look for another one in the dereference hierarchy for the object. The second one will fail to compile and alert the developer.

The same applies to match ergonomics vs. explicit binding modes. (a, b) matches (&Type, &Type), but also &(Type, Type). If I have explicit patterns like &(ref a, ref b) it will fail to compile when the types change.

The multi-mode destructuring capabilities come in when you do things like

let Self {
    ref mut cache,
    ref mut storage,
    ref ids,
} = *self;

where you get exhaustiveness checking, explicit mode checking, and IDE highlighting of mutable bindings at the top of a function giving you detailed context about the things the function manipulates. This is very handy when your type dispatches to some free functions that manipulate the individual components. And that can happen in all more complex scenarios, like state machines:

match st {
    // lots of cases before
    SomeState { id, ref events, ref mut logger } if logger.traced(id) => ...
    SomeState { id, ref mut events, ... } => ...
    // lots of cases after
}

is a lot more expressive and clean to me than

match &mut st {
    // lots of cases before
    SomeState { id, events, logger } if logger.traced(*id) => ...
    SomeState { id, events, ... } => ...
    // lots of cases after
}

The former gives a lot more context to what's going on, applies restrictions (why even give the ability to the code block to mess with the bound id value?)) and the mutated parts are highlighted in the IDE.

And with all language-level restrictions, since they are at the language level instead of just convention, you can count on them to kick in in unsafe code.

@VitWW

@phaylon Just for clarity: this RFC do not propose anything to remove.
It propose add new kind of binding for "not ref" - move/value , for example let (value x, value mut y) = v

I am aware, apologies for not mentioning it. As you can tell I'm all for more explicitness and control :) It was more intended as a defense of the general idea of control in general, instead of a rebuke of the advancement.

Edit: I actually just checked and it seems VSCode doesn't even highlight the bindings in all cases. It must be my brain inserting that when I see the ref muts.

@stepancheg
Copy link

@H2CO3

You keep coming back to whether ref was used or not. But it doesn't matter, it's not the point. The point is that explicitness with the type helps.

Well, you have replied to my comment about proposal to remove ref:

Rely solely on match ergonomics and deprecate the ref keyword.

You said, quoting:

That would be massively harmful, primarily in unsafe code

So I'm asking for explanation/illustration, how exactly removal of ref would be massively harmful in unsafe code.

But this is a red herring, it doesn't change either the problem or the solution in any meaningful way.

Equally incorrect code can be easily written with ref: example.

@phaylon
Copy link

phaylon commented Apr 21, 2023

Anyway, now that I've given my defense of explicit binding modes, I'll go back to lurking. I hope it will help demonstrate the value it has to some of us, and how expanding the ways to fully qualify patterns as the proposal attempts are things worthy of consideration.

And again: Apologies to OP for it being more expansive than I had anticipated.

@VitWW
Copy link

VitWW commented Jul 29, 2024

While Proposal #3627 (Match ergonomics 2024) is accepted, is this proposal still valid?
Is it need to update?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
Status: Rejected/Not lang
Development

Successfully merging this pull request may close these issues.

None yet