RFC idea: Result<T, !> implementing Deref<T>

I just had a crazy idea that might be RFC material, but I wanted feedback from the community first.

There are, in the standard lib and in other crates, a number of traits with the following structure:

pub trait MyTrait {
    type Error: std::error::Error;
    fn my_method(&self) -> Result<Foo, Self::Error>;
}

Some implementations of this trait will never fail when calling my_method. Those are encouraged to use Infallible or the unstable never for MyTrait::Error.

Still, when I call my_method on a value which I know can not fail, I have to unwrap the result, which is verbose and annoying.

    let bar = Bar::new(); // Bar: MyTrait<Error=Infallible>
    bar.my_method().unwrap().foo_method();

Here is the idea: Result<T, Infallible> and Result<T, !> could implement Deref<Target=T> and DerefMut<Target=T>. So the code above could be written more simply:

    bar.my_method().foo_method();

What do you think?

internals.rust-lang.org is the more appropriate forum for posts like these, since it is about Rust itself and not how to use Rust.

That aside, there are a few problems with this:

  • The deref trait is specifically for smart pointers, and should only be used for them (docs).
  • Rust currently lacks a DerefMove trait, meaning that the Ok value can't actually be moved out of the Result (only taken a reference/mutable reference to) without special-casing Result in the language like Box. This makes the feature not useful for many scenarios.
  • Eventually Rust will allow you to let Ok(x) = infallible_result;, which mostly solves the problem. It's can't be done in one expression, so isn't ideal, but it works.
4 Likes

Other than the circular “implements Deref”, what is the definition of a smart pointer here? Result<T,!> seems plausibly like one to me— it’s almost an alias for Box.

Result is not a pointer, therefore it is not a smart pointer either.

A smart pointer is generally something that internally points to some heap allocation, but provides some automatic memory management capabilities (usually via its constructor and destructor).

2 Likes

Oh no. More "line noise" syntax.

2 Likes

That definition feels unsatisfying to me, because it operates on a lower abstraction level than I’m expecting for Rust types. I tend to think of anything that mediates access to a single well-defined object as a smart pointer, independent of where that object is stored.

By your definition, something like smallbox isn’t a smart pointer because it might store its contents on the stack. Similarly, a MutexGuard backed by a stack-allocated Mutex isn’t a smart pointer. In fact, the example from the book contains its referent inline.

2 Likes

Note: there’s one big difference between the MyBox<T> type we’re about to build and the real Box<T> : our version will not store its data on the heap. We are focusing this example on Deref , so where the data is actually stored is less important than the pointer-like behavior.

I think that such "infallible results" can arguably be considered as smart pointers -- and others have expressed a similar opinion in this thread.

Granted, in some situations (namely, when a move is required), one would still have to explicitly unwrap the result. But that would still make developer's life easier in many other situations.

It does not solve my problem, in the sense that it forces me to explicitly deal with a result (by writing Ok(x)) while the value can not be an error.

Until let Ok(x) = infallible_result; works, we have let x = infallible_result.into_ok(); (though still unstable).

2 Likes

MutexGuard is a smart pointer because it contains a pointer to the mutex, whereever it is.

Smallbox is a smart pointer because it can contain a pointer to the data. Although it can also store it inline, heap allocation is the default case.

Result is not a smart pointer because it never contains a pointer to the data, as it by design always stores the data inline.

I am not opposed to this idea. I just think that Deref is a hacky way to do it. I think it would be best for Rust to introduce a Coerce trait which is blanket-implemented for all T: Deref so we can have the ergonomics of coercion without the restrictions of Deref.

Edit: Actually on further thought Coerce is a bad idea. Std already has too many traits and this would just introduce more complexity and confusion. Perhaps Rust could auto-coerce traits like Into and AsRef?

I think this is hacky. Add an into_ok() that only compiles if the error is an empty enum, and fix this such that it compiles:

let Ok(val) = my_infallible_result;

Whether or not it can conceptually be considered one -- I think reasonable people can disagree there -- the fact is that the current Result type hasn't been treated like something that will have Deref, so adding it now is probably not going to happen.

The way you can tell? All of the methods that it has.

Compare, for example, ManuallyDrop::into_inner with MaybeUninit::assume_init. Conceptually these are similar operations: unwrap the value it holds. But ManuallyDrop is Deref, so it's not a method, it's an associated function. Whereas MaybeUninit is not Deref, so it's a method.

Result has too many methods that would be surprises should it be Deref. For example, calling .transpose() on a Result<Matrix, !> would fail, because it would hit the Result version.

Note that there are many people who appreciate that Rust does not even implicitly widen integers (u16 -> u64, and such). Given that, I suspect it'd be a non-starter to even consider expanding coercions to arbitraryily-complicated and frequently-allocating things -- like String: From<&str> being a good example. Doing that kind of thing implicitly would also imply that Cloneing should be implicit too, another thing where many people are happy that Rust makes it visible in the code.

EDIT: Something like AsRef might be plausible, though. Those are, like Deref, restricted enough that most of the complaints about Into don't hold.

2 Likes

Very good point! Thanks for this insight.

2 Likes