Lived Experiences: Strange match ergonomics


Continuing the discussion from Allow disabling of ergonomic features on a per-crate basis?:

There are a few reports of match ergonomics that produce surprising or counter-intuitive results. It seems, in effect, that match ergonomics may fall into the same uncanny valley as 2015 modules. While guided by simple rules, the implications of those rules are non-obvious.

I’m interested in knowing which patterns everyone finds surprising. Even better (though it’s a lot of effort), would be a screencast or writeup that demonstrates you running into surprising behavior while editing code. If there are particular patterns that cause trouble, these could be used to improve compiler guidance, especially adding lints that offer guidance to bring non-obvious elision to the fore.

let S { ref y } = *x and reference wrapping

Thanks to by @novacrazy on the disable ergonomics thread.

Consider the following:

struct SomeStruct { y : SomeOtherStruct }

#[derive(Clone, Copy)]
struct SomeOtherStruct { z: i32 }

impl SomeOtherStruct { fn frob(&self) { println!("frobbed {:?}", self.z) } }

pub fn main() {
    let x = SomeStruct { y: SomeOtherStruct { z: 5 } };
    let SomeStruct { y } = x;
    foo(y) // outputs "5"

fn foo(b: SomeOtherStruct) {
    println!("{}", b.z)

The binding to y is performed by copying SomeOtherStruct, and y.frob() is called using automatic reference mapping. The code compiles without error.

Now consider what occurs if x borrows SomeStruct instead of owning it:

pub fn main() {
    let x = &SomeStruct { y: SomeOtherStruct { z: 5 } };
    let SomeStruct { y } = x;
    foo(y) // expected struct `SomeOtherStruct`, found reference

By simply changing adding character, the semantics of the match change. y is no longer a copy; it’s a borrow, because the match desugars to let &SomeStruct { ref y } = x;. Automatic reference mapping continues to obscure the reference, and the compiler emits an error when calling foo(y), which is surprising.

The change in match behavior is not entirely obvious, due to the interaction between the match, automatic reference mapping, and Copy.

In isolation, this case may not seem to be a big deal–the author of the code might be expected to understand the implications of the borrow. Real-world cases, however, suffer from additional complexity, especially in cases of refactoring deep copies into borrows.

Case :face_with_raised_eyebrow:: let Foo(x): &Foo<i32> = &&Foo(23);

@phaylon and I are also discussing a potentially related case on reddit. I am not experienced enough with Rust to tease it apart, and no variant removing ergonomics successfully typechecks.