Pre-RFC: Add Result::pass(), turning Result<T,E> into Result<U,F>, if From is set up


#1
  • Feature Name: result-pass
  • Start Date: (fill me in with today’s date, YYYY-MM-DD)
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Result currently provides no convenient way to convert between Results, even if a conversion between the wrapped values is defined. This makes it much more involved to pass on full Result values, then using try! or ? for passing on plain error values.

This RFC suggests adding a method pass(), which converts Result<T,E> into Result<U,F> if T: Into<U> and E: Into<F>.

Motivation

Consider the following code:

#[derive(Debug)]
enum ServerError {
    IoError(std::io::Error),
    // some more cases
}

impl From<std::io::Error> for ServerError {
    fn from(e: std::io::Error) -> ServerError {
        ServerError::IoError(e)
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let res = stream.map_err(ServerError::from)
                  .and_then(|mut s| {
                      handle(&mut s)
                  });
        
        if let Err(e) = res {
            println!("Error occured: {:?}", e);
        }
    }
}

fn handle(stream: &mut TcpStream) -> Result<(), ServerError> {
    write!(stream, "hello!").map_err(ServerError::from)
}

Note the calls to map_err, which only provide trivial conversion between errors. Finding this pattern is non-trivial and its application unnecessarily repetitive.

While Result provides a lot of convenience methods to be combined, it does not provide a trivial way to be converted into other Result, even if implementors followed good practice and provide std::convert implementations.

Similar arguments are apply for the T value.

Detailed design

Add a method pass to Result, with the following implementation:

impl Result<T,E> {
    fn pass<U,F>(self) -> Result<U,F> where T: Into<U>, E: Into<F> {
        self.map(T::into).map_err(E::into)
    }
}

This allows easy, non-involved conversion between Result types that have their std::convert story in order.

As T implies T: From<T>, this also neatly applies when the error or the Result stay the same.

How We Teach This

This is a method addition that should come with its own documentation, and additional documentation in std::result. An addition to the error handling practices in the book can be considered.

Drawbacks

Result already has a big interface and this adds a method to it.

Alternatives

Implement From<Result<T,E>> for Result<U,F> where U: From<T>, F: From<E>.

This is more general, but possibly harder to discover, as the appropriate method to call would end up into().

Unresolved questions

:bike::house_with_garden:: Is pass the best name?


#2

Yes! I’ve definitely hit this. There are certain times when ? just isn’t convenient.

It might be possible to do this via Into itself (so you’d write blah.into() to get the conversion), but I’m not sure offhand whether that will fly with trait coherence.

Otherwise, if we need a dedicated method, pass doesn’t quite evoke this functionality to me. Unfortunately, nothing better is immediately leaping to mind…


#3

I’m hitting this all the time, as I love to use .and_then and friends.

My biggest fear there is that there’s an overlapping implementation of From<Result…> somewhere, but I wouldn’t know of any. Also, it might be worthwhile to investigate an extension to TryFrom.

Let the bikeshed begin :).


#4

I ended up posting this to the RFC repos.


#5

This would conflict with the diagonal impl<T> From<T> for T impl. Tho lifting From over Option and Result is one of the main things that want lattice specialization.


#6

Also, impl<T:Into<U>> From<Async<T>> for Async<U> came up in the ? extension RFC as yet another case needing this.


#7

Can you point me to a specific comment?

Also, is this just a similarity or does this need the impl From... for Result solution?