On the necessity of the at operator in pattern matching

Hi.

In a match arm, if we want to use the value we are testing for pattern matching, one currently needs to use the @ operator to bind it to a new variable. Could the compiler automatically create a new variable with the same name as the identifier we are matching against?

Let's consider this example from The book:

    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id_variable @ 3..=7 } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => println!("Found an id in another range"),
        Message::Hello { id } => println!("Found some other id: {}", id),
    }

Would it be possible/preferable to be able to rewrite the first arm as follows?

Message::Hello { id: 3..=7 } => println!("Found an id in range: {}", id),

The compiler could then automatically expand it to

Message::Hello { id: id @ 3..=7 } => println!("Found an id in range: {}", id),

From my understanding, the compiler already creates a variable with the same name as an identifier in the third arm, so I do not see any fundamental reason why we couldn't do it as well for the first arm.

3 Likes

Well, it’d be a breaking language change, since the syntax already exists and doesn’t introduce a variable (hence it doesn’t shadow any variables of the same name that may already be in scope, either).

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 11 };

    let id = 42;

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Found an id in range: {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range; and the (unrelated) variable named `id` contains: {}", id)
        }
        Message::Hello { id } => println!("Found some other id: {}", id),
    }
}

Further, if multiple instances of the same name appear, currently you get an error

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg1 = Message::Hello { id: 11 };
    let msg2 = Message::Hello { id: 11 };

    let id = 42;

    match (msg1, msg2) {
        (
            Message::Hello {
                id: id_variable1 @ 3..=7,
            },
            Message::Hello {
                id: id_variable2 @ 3..=7,
            },
        ) => println!("Found an ids in range: {} {}", id_variable1, id_variable2),
        (Message::Hello { id }, Message::Hello { id }) => println!("fallback"),
    }
}
error[E0416]: identifier `id` is bound more than once in the same pattern
  --> src/main.rs:20:50
   |
20 |         (Message::Hello { id }, Message::Hello { id }) => println!("fallback"),
   |                                                  ^^ used in a pattern more than once

and it’s be bad if the same error was hit for all patterns involving id: ….

Finally, for non-Copy values, there’s also errors like the following you can encounter with an @ pattern

fn main() {
    struct NotCopy;
    struct S {
        field: (NotCopy, NotCopy),
    }
    let value = S {
        field: (NotCopy, NotCopy),
    };
    match value {
        S {
            field: field @ (x, y),
        } => (),
    }
}
error[E0382]: use of partially moved value: `value.field`
  --> src/main.rs:11:20
   |
11 |             field: field @ (x, y),
   |                    ^^^^^       - value partially moved here
   |                    |
   |                    value used here after partial move
   |
   = note: partial move occurs because `value.field.1` has type `NotCopy`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
   |
11 |             field: ref field @ (x, ref y),
   |                    +++             +++

and we obviously wouldn’t want to break/prohibit code like

fn main() {
    struct NotCopy;
    struct S {
        field: (NotCopy, NotCopy),
    }
    let value = S {
        field: (NotCopy, NotCopy),
    };
    match value {
        S { field: (x, y) } => (),
    }
}
11 Likes

What about Message::Hello { id @ 3..=7 } becoming syntax sugar for Message::Hello { id: id @ 3..=7 }? AFAIK this is currently invalid syntax.

13 Likes

I see. Thank you for the counter-examples. It's nice to have them to understand the rationale behind the current design.

(NOT A CONTRIBUTION)

Note another reason is that you could have a conflicting binding in the pattern; e.g.

struct Foo {
    bar: Option<String>
}

fn f(foo: Foo) {
    if let Foo { bar: Some(baz) } = foo {
       // moved the string into both `bar` and `baz`
    }
}

(EDIT: Realized steffahn covers this in the later example.)

Still, I've seen users tripped up by this surprisingly often - mainly because they rarely need to use the @ otherwise I think - and if I were to do over the syntax from scratch I would consider some different rules.

Agreed, I think this would be a very intuitive extension to the current shortcut syntax.

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