rfcs/text/3627-match-ergonomics-2024.md

18 KiB
Raw Permalink Blame History

Summary

Various changes to the match ergonomics rules:

  • On edition ≥ 2024, & and &mut patterns only remove a single layer of references.
  • On edition ≥ 2024, mut on an identifier pattern does not force its binding mode to by-value.
  • On all editions, & patterns can match against &mut references.
  • On all editions, the binding mode can no longer ever be implicitly set to ref mut behind an & pattern.

Motivation

Match ergonomics have been a great success overall, but there are some surprising interactions that regularly confuse users.

mut resets the binding mode

mut resets the binding mode to by-value, which users do not expect; the mutability of the binding would seem to be separate concern from its type (https://github.com/rust-lang/rust/issues/105647, https://github.com/rust-lang/rust/issues/112545).

let (x, mut y) = &(true, false);
let _: (&bool, bool) = (x, y);

Cant cancel out an inherited reference

& and &mut patterns must correspond with a reference in the same position in the scrutinee, even if there is an inherited reference present. Therefore, users have no general mechanism to “cancel out” an inherited reference (https://users.rust-lang.org/t/reference-of-tuple-and-tuple-of-reference/91713/6, https://users.rust-lang.org/t/cannot-deconstruct-reference-inside-match-on-reference-why/92147, https://github.com/rust-lang/rust/issues/50008, https://github.com/rust-lang/rust/issues/64586).

fn foo(arg: &(String, Vec<i32>, u8)) {
    // We want to extract `&String`, `&Vec`, and `u8` from the tuple.
    let (s, v, u) = arg; // u is &u8, not what we wanted
    let &(ref s, ref v, u) = arg; // we have to abandon match ergonomics entirely
}

A single & can strip two references

When an & or &mut pattern is used in a location where there is also an inherited reference present, both are stripped; adding a single & to the pattern can remove two &s from the type of the binding.

let [a] = &[&42]; // a = &&42
let [&a] = &[&42]; // a = 42

Guide-level explanation

Match ergonomics works a little differently in edition 2024 and above.

mut no longer strips the inherited reference

mut on a binding does not reset the binding mode on edition ≥ 2024. Instead, mut on a binding with non-default binding mode is an error.

//! Edition ≥ 2024
//let (x, mut y) = &(true, false); // ERROR

& matches against &mut

On all editions, & patterns can match against &mut references. On edition 2024 and above, this includes "inherited" references as described below.

//! All editions
let &foo = &mut 42;
let _: u8 = foo;
//! Edition ≥ 2024
let [&foo] = &mut [42];
let _: u8 = foo;

Matching against inherited references

In all editions, when you match against an & or &mut reference with the type of its referent, you get an "inherited reference": the binding mode of "downstream" bindings is set to ref or ref mut.

//! All editions
// `x` "inherits" the `&` from the scrutinee type.
let [x] = &[42];
let _: &u8 = x;

In edition 2024 and above, an & or &mut pattern can match against this inherited reference, consuming it. A pattern that does this has no other effect.

//! Edition ≥ 2024

// `&` pattern consumes inherited `&` reference.
let [&x] = &[42];
let _: u8 = x;

// Examples from motivation section

fn foo(arg: &(String, Vec<i32>, u8)) {
    let (s, v, &u) = arg;
    let _: (&String, &Vec<i32>, u8) = (s, v, u);
}

let [&x] = &[&42];
let _: &u8 = x;

Reference-level explanation

This explanation assumes familiarity with the current match ergonomics rules, including the "default binding mode" terminology. Refer to RFC 2005.

The rules in brief

Building on the rules of RFC 2005, this RFC adopts the following five rules for match ergonomics:

  • Rule 1: When the DBM (default binding mode) is not move (whether or not behind a reference), writing mut on a binding is an error.
  • Rule 2: When a reference pattern matches against a reference, do not update the DBM.
  • Rule 3: If we've previously matched against a shared reference in the scrutinee (or against a ref DBM under Rule 4, or against a mutable reference treated as a shared one or a ref mut DBM treated as a ref one under Rule 5), set the DBM to ref whenever we would otherwise set it to ref mut.
  • Rule 4: If an & pattern is being matched against a non-reference type or an &mut pattern is being matched against a shared reference type or a non-reference type, and if the DBM is ref or ref mut, match the pattern against the DBM as though it were a type.
  • Rule 5: If an & pattern is being matched against a mutable reference type (or against a ref mut DBM under Rule 4), act as if the type were a shared reference instead (or that the ref mut DBM is a ref DBM instead).

Rule 1 and Rule 2 are edition-dependent and will be stabilized with Rust 2024. The other three rules will be stabilized in all editions.

In the sections below, we describe these rules and their effects in more detail.

Edition 2024: mut does not reset binding mode to by-value

In the new edition, mut no longer resets the binding mode to by-value; instead, mut on a binding with a by-reference binding mode is an error.

//! Edition ≥ 2024
// let [mut a] = &[42]; //ERROR

All editions: & patterns can match against &mut references

& patterns can match against &mut references.

//! All editions
let &foo = &mut 42;
let _: u8 = foo;

However, the ref mut binding mode cannot be used behind such patterns.

//! All editions
let &ref mut foo = &mut 42;
//  ^~ERROR: replace `&` with `&mut `
let _: &mut u8 = foo;

However, if the type of the scrutinee is unknown, an & pattern will still constrain inference to force it to be a shared reference.

//! All editions
fn generic<R: Ref>() -> (R, bool) {
    R::meow()
}

trait Ref: Sized {
    fn meow() -> (Self, bool);
}

impl Ref for &'static [(); 0] {
    fn meow() -> (Self, bool) {
        (&[], false)
    }
}

impl Ref for &'static mut [(); 0] {
    fn meow() -> (Self, bool) {
        (&mut [], true)
    }
}

fn main() {
    let (&_, b) = generic();
    assert!(!b);
}
//! All editions
fn generic<R: Ref>() -> R {
    R::meow()
}

trait Ref: Sized {
    fn meow() -> Self;
}

impl Ref for &'static mut [(); 0] {
    fn meow() -> Self {
        &mut []
    }
}

fn main() {
    let &_ = generic(); //~ERROR[E0277]: the trait bound `&_: Ref` is not satisfied
}

Edition 2024: & and &mut can match against inherited references

When the default binding mode is ref or ref mut, & and &mut patterns can reset it. & patterns will reset either ref or ref mut binding modes to by-value, while &mut can only reset ref mut. An & or &mut pattern that resets the binding mode in this way has no other effect.

//! Edition ≥ 2024

let [&x] = &[3u8];
let _: u8 = x;

let [&mut x] = &mut [3u8];
let _: u8 = x;

let [&x] = &mut [3u8];
let _: u8 = x;
//! All editions
//let [&mut x] = &[3u8]; // ERROR

& patterns are otherwise unchanged from older editions.

//! All editions

let &a = &3;
let _: u8 = a;

//let &b = 17; // ERROR

If the default binding mode is ref, then &mut patterns will not be able to match against it, so they will match structurally instead (preserving the binding mode).

//! Edition ≥ 2024
let [&mut x] = &[&mut 42];
let _: &u8 = x;

&mut patterns are otherwise unchanged.

//! All editions

let &mut x = &mut 3;
let _: u8 = x;

let &mut x = &mut &mut 3;
let _: &mut u8 = x;

let &mut x = &mut &&mut 3;
let _: &&mut u8 = x;

//let &mut x = &&mut 3; // ERROR

All editions: the default binding mode is never set to ref mut behind an & pattern or reference

The binding mode is set to ref instead in such cases. (On older editions, this allows strictly more code to compile.)

//! All editions (new)

let &[[a]] = &[&mut [42]];
let _: &u8 = a; // previously `a` would be `&mut u8`, resulting in a move check error

let &[[a]] = &mut [&mut [42]];
let _: &u8 = a;
//! Edition ≥ 2024

let &[[&a]] = &[&mut [42]];
let _: u8 = a;

//let &[[&mut a]] = &[&mut [42]]; // ERROR

Migration

This proposal, if adopted, would allow the same pattern to have different meanings on different editions:

let [&a] = &[&0u8]; // `a` is `u8` on edition ≤ 2021, but `&u8` on edition ≥ 2024
let [mut a] = &[0u8]; // `a` is `u8` on edition ≤ 2021, but `&u8` on edition ≥ 2024

Instances of such incompatibilities appear to be uncommon, but far from unknown (20 cases in rustc, for example). The migration lint for the feature entirely desugars the match ergonomics of the affected pattern. This is necessary to produce code that works on all editions, but it means that adopting the new rules could require editing the affected patterns twice: once to desugar the match ergonomics before adopting the new edition, and a second time to restore match ergonomics after adoption of the new edition.

Macro subpatterns

Unfortunately, when a subpattern derives from a macro expansion, fully desugaring the match ergonomics may not be possible. For example:

//! crate foo (edition 2021)
#[macro_export]
macro_rules! foo {
    ($foo:ident) => {
        [$foo]
    };
}
//! crate bar (edition 2021, want to migrate to 2024)
extern crate foo;
use foo::*;

fn main() {
    let ([&x], foo!(y)) =  &([&0], [0]);
    //~^ WARN: the semantics of this pattern will change in edition 2024
    let _: i32 = x;
    let _: &i32 = y;
}

In such cases, there is no possible machine-applicable suggestion we could emit to produce code compatible with all editions (short of expanding the macro). However, such code should be extremely rare in practice.

Drawbacks

This is a silent change in behavior, which is considered undesirable even over an edition.

Rationale and alternatives

Desirable property

The proposed rules for new editions uphold the following property:

For any two nested patterns $pat0 and $pat1, such that $pat1 uses match ergonomics only (no explicit ref/ref mut), and pattern match let $pat0($pat1(binding)) = scrut, either:

  • let $pat0(temp) = scrut; let $pat1(binding) = temp; compiles, with the same meaning as the original composed pattern match; or
  • let $pat0(temp) = scrut; let $pat1(binding) = temp; does not compile, but let $pat0(ref temp) = scrut; let &$pat1(binding) = temp; compiles, with the same meaning as the original composed pattern match.

In other words, the new match ergonomics rules are compositional.

mut not resetting the binding mode

Admittedly, there is not much use for mutable by-reference bindings. This is true even outside of pattern matching; let mut ident: &T = ... is not commonly seen (though not entirely unknown either). The motivation for making this change anyway is that the current behavior is unintuitive and surprising for users.

Never setting default binding mode to ref mut behind &

We cant delay this choice

Patterns that work only with this rule

//! All editions: works only with this rule
let &(i, j, [s]) = &(63, 42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
//! Edition ≥ 2024: works with or without this rule (alternative to above)
let (&i, &j, [s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
//! All editions: works with or without this rule (alternatives to above)
let &(i, j, [ref s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
let &(i, j, &mut [ref s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String

Patterns that work only without this rule

//! Edition ≥ 2024: works only without this rule
let &[[&mut a]] = &[&mut [42]]; // x: i32
// `&mut` in pattern needs to match against either:
// - `&mut` in value at same position (there is none, so not possible)
// - inherited `&mut` (which the rule downgrades to `&`)
//! Edition ≥ 2024: works with or without this rule (alternatives to above)
let &[[&a]] = &[&mut [42]]; // x: i32
let &[&mut [a]] = &[&mut [42]]; // x: i32

Makes behavior more consistent

On all editions, when a structure pattern peels off a shared reference and the default binding mode is already ref mut, the binding mode gets set to ref:

//! All editions
let [a] = &mut &[42]; // x: &i32

But when the binding mode is set to ref, and a mutable reference is peeled off, the binding mode remains ref:

//! All editions
let [a] = &&mut [42]; // x: &i32

In other words, immutability usually takes precedence over mutability. This change, in addition to being generally useful, makes the match ergonomics rules more consistent by ensuring that immutability always takes precedence over mutability.

Ensures that a desirable property is preserved

The current match ergonomics rules uphold the following desirable property:

An &mut pattern is accepted if and only if removing the pattern would allow obtaining an &mut value.

For example:

//! All editions
let &mut a = &mut 42; // `a: i32`
let a = &mut 42; // `a: &mut i32`

let &[&mut a] = &[&mut 42]; // `a: i32`
//let &[a] = &[&mut 42]; // ERROR, but…
let &[ref a] = &[&mut 42]; // `a = &&mut i32` (so we did manage to obtain an `&mut i32` in some form)

Adopting the "no ref mut behind &" rule ensures that this property continues to hold for edition 2024:

//! Edition ≥ 2024
let &[[&mut x]] = &[&mut [42]]; // If we were allow this, with `x: i32` …
//let &[[x]] = &[&mut [42]]; // remove the `&mut` → ERROR, if the default binding mode is to be `ref mut`
// nothing we do will get us `&mut i32` in any form

& patterns matching against &mut

There are several motivations for allowing this:

  • It makes refactoring less painful. Sometimes, one is not certain whether an unfinished API will end up returning a shared or a mutable reference. But as long as the reference returned by said API is not actually used to perform mutation, it often doesnt matter either way, as &mut implicitly reborrows as & in many situations. Pattern matching is currently one of the most prominent exceptions to this, and match ergonomics magnifies the pain because a reference in one part of the pattern can affect the binding mode in a different, faraway location1. If patterns can be written to always use & unless mutation is required, then the amount of editing necessary to perform various refactors is lessened.
  • Its intuitive. &mut is strictly more powerful than &. Its conceptually a subtype, and even if not implemented that way2, coercions mean it often feels like one in practice.
let a: &u8 = &mut 42;

Versus "eat-two-layers"

An alternative proposal would be to allow & and &mut patterns to reset the binding mode when not matching against a reference in the same position in the scrutinee, but to not otherwise change their behavior. This would have the advantage of not requiring an edition change. However, it would remain confusing for users. Notably, the property from earlier would continue to not be satisfied.

In addition, this approach would lead to tricky questions around when mutabilities should be considered compatible. And there would be compatibility concerns with certain proposals for "deref patterns".

(This alternative is currently implemented under a separate feature gate.)

Unresolved questions

  • How much churn will be necessary to adapt code for the new edition? There are 0 instances of affected patterns in the standard library, and 20 in the compiler, but that is all the data we have at the moment.

Future possibilities

  • An explicit syntax for mutable by-reference bindings should be chosen at some point, along with removing the prohibition on implicitly by-reference mutable bindings.
  • Future changes to reference types (partial borrows, language sugar for Pin, etc) may interact with match ergonomics.

Deref patterns

Because it is compositional, the “eat-one-layer” model proposed by this RFC is fully compatible with proposals for "deref patterns", including allowing &/&mut patterns to match against types implementing Deref/DerefMut. One question that would need to be resolved is whether and how deref patterns (explicit or implicit) affect the default binding mode.

Matching &mut directly behind &

There is one notable situation where match ergonomics cannot be used, and explicit ref is required. This happens where &mut is nested behind &:

// No way to avoid the `ref`, even with this RFC
let &&mut ref x = &&mut 42; // x: &i32

There are two strategies we could take to support this:

  • &mut patterns could “strip off” outer &. For example, in let &mut x = &&mut 42;, the &mut pattern would match the &mut reference in the scrutinee, leaving & to be inherited and resulting in x: &i32.
    • This may not extend gracefully to future language features (partial borrows, for example) as it potentially relies on reference types forming a total order.
  • The compiler could insert &mut ref in front of identifier patterns of type &mut that are behind an & pattern. For example, let &x = &&mut 42; would be transformed into let &&mut ref x = &&mut 42;.
    • The full desugaring would be more complicated, as it would need to handle @ patterns.

  1. This is especially true in light of the new rule that prevents the default binding mode from being set to ref mut behind &. ↩︎

  2. Making &mut a subtype of & in actual implementation would require adding significant complexity to the variance rules, but I do believe it to be possible. ↩︎