Suggestion unwrap_safe for Result<T, !>, or `impl From<Result<T, !>> for T`

I’ve noticed that people have started to evaluate crate quality by looking for branches that may panic and seeing if they have been audited as impossible. So I propose adding something like

impl<T> Result<T, !> {
    fn unwrap_safe(self) -> T {
        self.unwrap()
    }
}

which saves you having to explain that the unwrap is audited safe in a comment.

Thoughts?

An example of current practice from embedded-hal (the comment would become unnecessary, reducing cognitive load for the reviewer)

            await!(timer.wait()).unwrap(); // NOTE(unwrap) E = Void

I’m definitely supportive of something like this but the implementation should be evocative of the very safety it tries to provide: instead of merely delegating to unwrap, I think it would be nicer to implement it with a non-fallible pattern match.

The name could be better, too – if I recall correctly, adding _safe to a method name is not really idiomatic in Rust; the no-panic guarantee of this method would also be unrelated to the kind of unsafety the language tries to protect programmers from. I would rather call it something like into_inner() that is pretty common for wrapper types which guarantee the ability to retrieve the wrapped value.

And while we are at it, would it be possible to impl From<Result<T, !>> for T? Then we could simply call .into() on an always-Ok result. (Not sure about the coherence rules here but I think it should work.)

6 Likes

Since ! should be isomorphic to enum Never {} (correct me if I am wrong), we could imagine a trait for their common behavior (naming could change):

trait UnReach : Into<!> {
  fn unreachable (self) -> ! { self.into() }
}
impl < Never : Into<!> > Unreach for Never {}

Where for all enum Never {} types, all we would have to is define/derive the Into<!> impl:

enum Never {}

impl Into<!> for Never {
  fn into(self) -> ! {
    match self {
      // ! /* there was a blog post about this */
    }
  }
}

We could then have:

/// in `::core`
impl <T, Never : Unreach>  From< Result<T, Never> > for T
{
  fn from (
    result: Result<T, Never>,
  ) -> Self
  {
    match result {
      Ok(inner) => inner,
      Err(never) => never.unreachable(),
    }
  }
}

EDIT: link to the ! pattern blog post: http://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/

In theory, yeah, but I’m guessing that once ! stabilizes, all uninhabited enums will disappear. Also, they exist in old code as a hack way of defining an external type, so I don’t know if these types could exist even thought its kinda breaking the language rules.

It would be awesome, and a better solution, if you could just use Into::into .

EDIT combined responses.

They would indeed become an obsolete pattern, but keeping the options open can still be useful: in the same vein that () does not make any zero-sized struct useless (even if we ignored new-type usage for traits) since they can convey richer information.

Take, for instance, the return type of ::std::iter::Iterator::next(), and let's call it IteratorState<Item>:

  • Currently, IteratorState<Item> is Option<Item>:

    type IteratorState<Item> = enum Option<Item>
    {
      Some(Item),
      None,
    }
    
  • with generators, it could be seen as ::std::ops::GeneratorState<Iterator::Item, ()>,

    i.e.,

    type IteratorState<Item> = enum GeneratorState<Item, ()>
    {
      Yielded(Item),
      Complete(()),
    }
    

    And although the one-type-fits-them-all is elegant logical-patterns-wise, that Complete(()) does not look nice (although quite better than the initial None!). With an (isomorphic) custom type, we could improve it:

  • enum IteratorState<T>
    {
      Yielded(T),
      Exhausted,
    }
    use self::IteratorState::*;
    impl<T> IteratorState<T> {
      fn some(self) -> Option<T> { match self {
        Yielded(value) => Some(value),
        Exhausted      => None,
      }}
    }
    

I thus think that custom never types may very well exist, either in the form

struct Never /* = */ (!);

or in the empty enum form. Hence the need for such a trait.

As an aside, I find custom inhabited types useful for "type-level enums", since it prevents using them as values.

Those are useful for truly immutable settings that can then lead to constant propagation and dead code removal:

trait Bool { const TRUTH: bool; fn is_true () -> bool { Self::TRUTH } }
  enum True  {} impl Bool for True  { const TRUTH: bool = true ; }
  enum False {} impl Bool for False { const TRUTH: bool = false; }

/// they can be "converted back to (zero-sized) values" with `PhantomData`
use ::std::marker::PhantomData;

struct Config<Verbose : Bool> {
  // ...
  _phantom: PhantomData<Verbose>,
}

impl<Verbose : Bool> Config<Verbose> {
  fn run (
    // ...
  )
  {
    if Verbose::is_true() { /* be verbose */ }
    // do some stuff
    if Verbose::is_true() { /* be verbose */ }
    // do more stuff
    if Verbose::is_true() { /* be verbose */ }
  }
}
3 Likes

Interesting, thanks for taking the time to write this.

1 Like

It’s not immediately clear what the value is (rather than more into inference… ugh). Result<T, !> is (or should be) represented as a T, so I doubt unwrap is more than a nop.

unwrap is a nop, as tested on the latest stable on playground. Even with empty enums like Void below.

playground link: https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=26f2231c576149736f2966dd41b6b045

#[derive(Debug)]
pub enum Void {}

type T = u32;

#[inline(never)]
fn abort<T>() -> T {
    panic!()
}

#[inline(never)]
pub fn unwrap_me_test(r: Result<T, Void>) -> T {
    match r {
        Ok(x) => x,
        Err(y) => abort()
    }
}
playground::unwrap_me_test:
	movl	%edi, %eax
	retq

<playground::Void as core::fmt::Debug>::fmt:
	ud2

As you can see playground::abort is nowhere to be seen because it was completely optimized away.

3 Likes

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