Provocative question: should you have to use `Result<T, !>`

Arguments for

  • More consistent
  • Probably simpler in the linter

Arguments against

  • Unnecessary unwrap clutter (error path will never be taken)

What alternative are you trying to argue against? Could you give some example code? It’s not really possible to engage with this post as currently written. I can only guess that you might not be aware of these two details:

  1. There’s been talk for a long time of making code like let r: Result<T, !> = Ok(t1); let t2: T = r; just work without an .unwrap() precisely because the type system can statically know that there’s no possibility of panicking there. AFAIK, this is actually part of the motivation of introducing ! as a single standard “type with zero values”/“never type” instead of everyone writing their own distinct zero-variant enum.

  2. You’re not expected to actually write things like Result<T, !> all that often yourself. For the most part, ! is only expected to show up as a result of generic code that has to handle non-! types being called from concrete code that knows it’s dealing with a special degenerate case–like an operation that’s normally fallible but in your application happens to be completely infallible–and we really want that generic code to get some extra optimizations that are only possible in the infallible case (like not generating any panic-handling code).

6 Likes

Yes, if a trait interface requires Result and allows you to choose the error, and you know you will never take the Err branch. Like in TryFrom::try_from

I must have missed that, would you happen to have a link? I would feel very disappointed if that would end up applying to all infallible patterns. For example, I wouldn’t want

struct Score(usize);
let score = Score(100);
let account = &accounts[score];

to compile. It would cancel out quite a lot of newtype safety.

Huh, I’ve never heard anyone suggest making newtypes/single-field structs coerce like that. The idea I’m talking about is (assuming I ever really understood it) making enums with all but one variant of type ! coerce to or just be equivalent to the type of the non-! variant.

(afaik nothing like this has actually been accepted yet, the original RFC for ! only suggests specialized methods with names like unwrap_nopanic).

If anyone’s interested the place I’ve seen this is using the nb crate for non-blocking embedded i/o.

pub enum nb::Error<E> {
    WouldBlock,
    RealError(E)
}

pub type nb::Result<T, E> = Result<T, nb::Error<E>>;

macro_rules! block {
    ($e:expr) => {
        loop {
           match $e {
                 Ok(t) => break Ok(t),
                 Err(Error::WouldBlock) => (),
                 Err(Error::Other(e)) => break Err(e)
            }
        }
    }
}

// implementation
pub fn blocking_but_never_fails() -> nb::Result<(), !> {
    writer_that_never_fails_but_sometimes_blocks.write(b'!')?;
    Ok(())
}

// usage
block!(blocking_but_never_fails()).unwrap(); // the unwrap is redundant here

Disclaimer The above code doesn’t compile and exists for demonstration only

That’s why I asked if you had a link :slight_smile: I think it might be fine if it were just happening apply to Result<T, !>, or if a type had to opt-in. I’m not sure I’d like it in general. An

enum Position<T, U> {
    Absolute(T),
    Relative(U),
}
type CurrentCase = Position<usize, !>;

shouldn’t simply be usable as a usize because the Relative variant is impossible.

Potentially related discussion: Add methods for unwrapping Result<T, !> / Result<!, E>

Fastest citations I could find:

https://github.com/rust-lang/rust/issues/35121#issuecomment-245826221 and many nearby comments on making ! affect exhaustiveness checking of match blocks

https://github.com/rust-lang/rust/issues/35121#issuecomment-248720913 suggests extending this to ! affecting what patterns are irrefutable, basically what I meant in my earlier post.

https://github.com/rust-lang/rust/issues/35121#issuecomment-360027186 explicitly leaves the irrefutable patterns thing out of the first stabilization attempt, moving it to https://github.com/rust-lang/rfcs/pull/1872

https://github.com/rust-lang/rfcs/pull/1872#issuecomment-358795038 closed that RFC on the grounds that it affects unsafe code in ways we don’t fully understand yet. Huh, I had no idea this was UCG-related!

Hopefully that’s enough to substantiate the “people have been talking about this” claim.

1 Like

I’m aware of the discussions regarding things like:

let r: Result<i32, !> = Ok(23);
let Ok(v) = r;
match r {
    Ok(v) => v,
}

which is a great feature, as it allows code to break at compile time when a pattern is no longer impossible, compared to the current situation of requiring a runtime panic.

What I haven’t seen is the

let r: Result<i32, !> = Ok(23);
let v: i32 = r;

part without any kind of unwrapping, which would worry me if not very constrained.

4 Likes

All the links in my previous post except the first are explicitly about this. Perhaps I should’ve pulled in exact quotes:

And from that closed RFC:

I can’t detect any mention of “constraints” in these discussions, other than the maybe implicit one that people seem to only be suggesting it for enums.

To be super clear, I have no strong opinion on whether this is a good or bad feature. I am merely claiming that this has been an explicit part of the discussion of the ! type for a long time (although that discussion is so vast it’s very easy to miss a detail like this), which should be undoubtable after seeing these quotes.

I did make a mistake in my first post by saying let t = r; instead of let Ok(t) = r;. No one appears to have ever proposed the former, and now that I’m thinking harder about all this, the latter does make way more sense to me.

2 Likes

Yeah, that was the only thing that surprised me :slight_smile: Thanks for clarifying!

2 Likes

If you use the pattern matching solution, you still have to write

let Ok(()) = my_func()