owned this note
owned this note
Published
Linked with GitHub
# design doc
## Participation in exhaustiveness
The following code would be unsound to consider the `match` exhaustive:
```rust
struct Foo(Vec<u8>, Cell<bool>);
impl Deref for Foo {
type Target = [u8];
fn deref(&self) -> &[u8] {
match self.1.get() {
true => &self.0[..],
false => {
self.1.set(true);
&[]
}
}
}
}
fn main() {
let foo = Foo(vec![10], Cell::new(false));
match foo {
// first call to `Foo::deref` returns `&[]`
*[_, ..] => return,
// second call to `Foo::deref` returns `&[10]
*[] => return,
}
// no match arms matched and now we're in unreachable code!! oops
}
```
However it is useful for some types to allow exhaustive matching for example `Box`, `Arc`, `Rc`, `Vec`, `String`, and `Pin<P>` where `P` is one of the previously mentioned types:
```rust
fn main() {
match vec![10, 12] {
*[_, _] => println!("two elements"),
*[_, _, _, ..] => println!("wow thats a lot"),
*[] | *[_] => println!("not much..."),
}
}
```
This match is sound to consider exhaustive as we know that `Vec`'s `Deref` impl always returns the same thing.
There are therefore two "kinds" of deref patterns/features
- types that have been "blessed" with knowledge of being idempotent(..?) so participate in exhaustiveness
- any old deref impl that act sort of like sugar for an `if let` guard or second let statement
### User code blessed deref
There are likely many good use cases for users being able to mark their own `Deref` types as unsafely being able to participate in exhaustiveness:
- smart pointers for interned things (common in compilers where pattern matching is _also_ extremely common)
- custom interior mutability types' guards (consider `parking_lot`)
## Implicit vs Explicit deref
Whether deref is explicit or implicit is (technically speaking) orthogonal to whether or not the deref pattern participates exhaustiveness checking or not. We could feasibly have both kinds explicit, or both kinds implicit, or user code ones implicit and std ones explicit or vice versa. It might not be orthogonal lang design wise however :-)
### Refactoring
If you have a let statement or match arm that is destructuring an `&T`/`&mut T` then changing the type to some other `Rc<T>` or `Arc<T>` or `MyUserRef<T>` etc would continue to work with no changes if there is no explicit annotation required which could make refactoring a lot less painful.
### Confusing not exhaustiveness participation
It might be confusing if a "simple" looking match errors because of it not participating in exhaustiveness, for example:
```rust
match blah {
A::Foo { .. } => ()
A::Bar => (),
}
```
If there are only two variants on `A` it might seem odd that this is not considered exhaustive if `blah` is only `T: Deref<Target = A>` instead of `&A`. If we make user code deref impls require explicit annotations then we can see immediately why its not exhaustive:
```rust
match blah {
*A::Foo { .. } => (),
*A::Bar => (),
}
```
This perhaps becomes better if derefs that participate in exhaustiveness do not require to be explicitly marked as a deref, as then the only time you would see `*` in code would be when you are no longer participating in exhaustiveness.
### Inference regressions
There is probably type inference regressions awaiting for implicit deref patterns, consider:
```rust
match <_>::default() {
"" => (),
_ => (),
}
```
This currently compiles as `_` is inferred to be `&str`, however with implicit deref patterns this could be inferred to any type where `<_ as Deref>::Target == &str`.
### Consistency with `&`/`&mut`
Currently matching on a type of `&`/`&mut` does not require writing out any kind of explicit "dereference" step:
```rust
fn main() {
match &10 {
10 => (),
_ => (),
}
}
```
It might be inconsistent to require matching on things that participate in exhaustiveness checking to add a deref annotation when `&`/`&mut` does not require it.
### Forwards Compat
If we decide to require explicit annotations always then we can always at a later date start allowing it to be implicit without it being a breaking change, except for inference breakage however this is a problem regardless of whether the explicit form is stabilized first.
However if we stabilize implicit deref patterns then we cant "undo" that decision until an edition, and it would also look a bit clown like to stabilize and then immediately decide "nope lets change this syntax".
## Subsets of the full feature
A subset of the full potential deref patterns functionality could be stabilized first to make common pain points using std types better without having to decide on more complicated things.
## Only std blessed types
Require explicit annotations to deref in patterns and only allow it for specially blessed std types. This is maximally fwds and backwards compatible as we can allow it being implicit in the future, and in the present do not have to worry about type inference regressions.
This cuts a bunch of ~~bikeshedding~~ design work:
- dont have to figure out what (or if) stable mechanism user derefs can use to participate in exhaustiveness
- dont have to make any "no takesie backsie" decisions aroung explicit/implicit deref patterns
## All types inexhaustively
Same as before except all types can explicitly annotate a deref in patterns but they wont participate in exhaustive unless it is a blessed std type.