How to get Result aligned with Result?

2020-04-08 14:28发布

问题:

I have this code

fn get_last_commit () -> String {

    Command::new("git")
            .arg("rev-parse")
            .arg("HEAD")
            .output()
            .map(|output| {
                String::from_utf8(output.stdout).ok().expect("error reading into string")
            })
            .ok().expect("error invoking git rev-parse")
}

I'd like to be able to cut the ok().expect(..) a bit down so that I ideally have something like that:

fn get_last_commit () -> String {

    Command::new("git")
            .arg("rev-parse")
            .arg("HEAD")
            .output()
            .and_then(|output| {
                String::from_utf8(output.stdout)
            })
            .ok().expect("error invoking git rev-parse")
}

However, that doesn't work because the errors don't line up leaving me with:

    mismatched types:
     expected `core::result::Result<_, std::io::error::Error>`,
        found `core::result::Result<collections::string::String, collections::string::FromUtf8Error>`
    (expected struct `std::io::error::Error`,
        found struct `collections::string::FromUtf8Error`)

I know the error handling changed quite a bit within the last month and I have the feeling there should be away to get them aligned without too much hassle. I seem unable to figure it out though.

回答1:

The problem is that the closure passed to the and_then needs to return a Result with the same error type as the Result that and_then was called on; otherwise, there's no single type that and_then could return; and_then maps one Ok type to another, but keeps the error type the same.

Since you are just throwing away the error value by converting it to an option with ok() that you unwrap anyhow, you can do that before calling and_then, and within the closure, as the Option type returned by and_then on an Option only depends on the value returned by the closure:

fn get_last_commit () -> String {

    Command::new("git")
            .arg("rev-parse")
            .arg("HEAD")
            .output()
            .ok()
            .and_then(|output| {
                String::from_utf8(output.stdout).ok()
            })
            .expect("error invoking git rev-parse")
}

If you actually cared about the error value, you would need to define your own error type that could contain either of the two types of errors, and wrap either of the errors up. The FromError trait and try! macro offer a convenient way to is wrap up the value and return it from one of several places in your function, though in this case map_err would probably be a better way to do that as you are doing it all via chained method calls rather than separate statements.

enum MyError {
    Io(IoError),
    Utf8(FromUtf8Error)
}

fn get_last_commit () -> Result<String,MyError> {

    Command::new("git")
            .arg("rev-parse")
            .arg("HEAD")
            .output()
            .map_err(MyError::Io)
            .and_then(|output| {
                String::from_utf8(output.stdout)
                       .map_err(MyError::Utf8)
            })
}

If you notice, this parallels the earlier solution fairly closely, coercing both of the result types into a single common type; in the first solution, it just throws away the error value by using ok(), while in the second, it preserves the error value so you can return it, but you now need the extra machinery of a type that could wrap either.