`!` in pattern position

A while ago, I remember @nikomatsakis and @RalfJung were talking about allowing ! to be used in match pattern positions. Doing so would allow/force the match branch to be omitted, making it possible to write code like this:

let result: Result<u32, !> = Ok(23);
let x = match result {
    Ok(x) => x,
    Err(!),     // no branch here!
}

Is this still the plan? I ask because I'm currently writing some code that makes heavy use of futures::select with futures that never terminate. So I have a lot of select cases that would be nice to be able to write like this:

futures::select! {
    x = do_something() => x,
    ! = daemon(),   // no branch here!
}

Also, in #1699 I proposed that if a trait impl method takes a ! as an argument then the user should be allowed to omit the method entirely. Some of the push-back against that proposal has been that it allows method impls to be mysteriously missing for reasons that aren't obvious to the person reading the code, and also that it ties into the larger, unsolved, problem of method impls that can't be called for other reasons (eg. due to unsatisfiable where clauses) and how we can allow such trait impls to be written.

If we allowed ! in pattern positions though it would at least solve the problem for !-arguments since we already allow patterns in argument positions. That is, we could just allow the method body to be omitted if ! is used in an argument pattern (in parallel with the match syntax above), like this:

trait TakeFoo {
    type Foo;
    fn take_foo(self, foo: Self::Foo);
}

impl TakeFoo for String {
    type Foo = !;
    fn take_foo(self, !);   // no body here!
}

So given that this feature seems pretty useful, is it still planned/wanted? Does need an RFC? Is any of it implemented? Is it far too early to do PRs for syn and futures to add support for it?

1 Like

I don't know if that is planned, but I would prefer the pattern to be irrefutable:

let result: Result<u32, !> = Ok(23);
let Ok(x) = result;

This doesn't work at the moment, but in the meantime, you can write

let result: Result<u32, !> = Ok(23);
let x = match result {
    Ok(x) => x,
    Err(never) => never,     // branch is never executed
}

I think this is still in the stage of "vague ideas in our minds".

@Aloso the problem is the interaction with other pattern-matching features such as auto-deref. When x: &!, we might not want match x {} to work.

The reason is that match x { y => ... } is supposed to be equivalent to { let y = x; ... }, but then it would be odd if for x: &!, we consider that match arm unreachable (and consider it UB to ever get there with unsafe code!), without doing the same with let.

What is particularly bad about match x {} is that there is something here that deref's x, checks its discriminant, and then declares UB because there can be no valid discriminant -- but there is no code that actually does that. That should be some way to "point at" the thing that causes the discriminant to be loaded. Hence the proposal to allow match x { &! /* no code */ }. Now there is a match arm to point at that determines that there is no possible discriminant, and there is an & pattern that explicitly dereferences the reference.

Even with that proposal, let Ok(x) = r is legal; we just automatically desugar it to let x = match r { Ok(x) => x, Err(!) }. So, we can separate the discussion of "how do matches on uninhabited types behave" from "how much do we do implicitly in that area", because we have some explicit syntax for uninhabited matching and then, orthogonally, can implicitly desugar things to add that syntax without people having to write it.

3 Likes

Another place I've long wanted a ! pattern is for closures, so you could write something like

let x = Ok::<u32, !>(6);
let y = x.unwrap_or_else(|!|);

(I feel like I had a better example of the utility of this, but I don't remember where it was).

Relevant note in the compiler:

1 Like

This pattern is the one allowing to get, for instance, non try functions out of their generic-over-the-error-type try_() implementations.

fn try_call<Ok, Err, F> (f: F) -> Result<Ok, Err>
where
    F : FnOnce() -> Result<Ok, Err>,
{ ... }

fn call<R, F> (f : F) -> R
where
    F : FnOnce() -> R
{
    try_call(|| Ok(f()))
        .unwrap_or_else(|!| {})
}

I personally use enum Void {} and |it: Void| match it {} in stable Rust.

1 Like

@Aloso

This doesn't work at the moment

It does actually, except that it's been feature gated for years behind #![feature(exhaustive_patterns)].

@RalfJung I'm not sure I understand your point about let y = x;. Are you saying that auto-deref could cause { let y = x; ... } to be interpreted as { let y = *x; ... }? (Coz I don't see how that's less likely to happen and cause an uninitialized read for any other type). Or are you saying that if x: &! then leaving out the let entirely could be considered equivalent to dereferencing x? (Coz I don't think that follows). Or are you just saying that { let y = x; ... } would make the ... unreachable? (Coz I think that does follow, but also that having x in scope should make the code unreachable to begin with).

If it's the latter can you give an example of some unsafe code which we'd like to consider valid but which could have a &! in scope? It's not obvious to me how someone could end up in that situation without doing something which would be wrong even for a type other than !. They could have a *const ! or MaybeUninit<!> in scope, but a &! could only be created from a raw pointer to an uninitialized ! or by using a reference to a partially-initialized struct containing a !, neither of which can happen if we require data behind a reference to always be fully-initialized.

Also, even if we want to allow users to have a &! in scope in live code, that doesn't effect the safety of match x {} vs match x { y => ... }. It just means that reaching match x {} would cause undefined behaviour since it dereferences x but match x { y => ... } wouldn't since it doesn't. In match x {} the user is explicitly dereferencing the &! and matching against it. They're doing it "explicitly" by leaving out code, but I don't see how they could do that by accident and have it pass type-checking (which would indicate that they're working specifically with ! and not some generic type parameter), and have it be invalid.

I'm sorry I don't follow. References are guaranteed to point to valid data, so &! and ! are pretty much equivalent.

When a value has the ! or &! type, this code is unreachable, so the compiler doesn't need to generate code for this. This means that Result<Foo, !> should have the same representation as

struct Result(Foo);

Furthermore, the compiler can eliminate all code paths where a variable with the ! type exists, so

match x: Result<Foo, !> {
    Ok(foo) => t,
    // No code emitted for this branch:
    Err(never) => {...}
}

IIUC, this means that (x: Result::<Foo, !>).unwrap() is a no-op.

1 Like

That's still up for debate:

1 Like

Sorry for the brevity, my comment was probably not very comprehensible. I felt like this was all already written up somewhere so I didn't want to spend the time to write it up again, but then I should have searched for the previous write-up instead of just leaving away all details.^^

So, I found the previous thread on the topic:

This should help resolve some of the confusion I caused. If there are still questions after reading that, please let me know. :slight_smile:

2 Likes

@RalfJung thanks, this really helped me understand the problem!

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