zombiesauce
zombiesauce

Reputation: 927

How can I convert a Result<T, E1> to Result<T, E2> when the question mark operator is ineffective?

Is there an idiomatic/concise way to convert Result<T, E1> to Result<T, E2> when E2 has implemented the From trait for E1?

I cannot use the ? operator because it doesn't compile.

In my case, E1 is ParseIntError, and E2 is a custom CalcPackageSizeError error enum.

playground

use std::error;
use std::fmt;
use std::io;
use std::io::Read;
use std::num::ParseIntError;

#[derive(Debug)]
enum CalcPackageSizeError {
    InvalidInput(&'static str),
    BadNum(&'static str),
}
impl error::Error for CalcPackageSizeError {}
impl fmt::Display for CalcPackageSizeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match *self {
                Self::InvalidInput(err_desc) => err_desc,
                Self::BadNum(err_desc) => err_desc,
            }
        )
    }
}

impl From<ParseIntError> for CalcPackageSizeError {
    fn from(_: ParseIntError) -> Self {
        CalcPackageSizeError::BadNum(
            "Error in calculating size of one or more of the packages involved.",
        )
    }
}

fn parse_comma_separated_num(num_str: &str) -> Result<usize, ParseIntError> {
    num_str
        .chars()
        .filter(|char| *char != ',')
        .collect::<String>()
        .parse::<usize>()
}

fn calc_all_package_size(contents: &str) -> Result<usize, CalcPackageSizeError> {
    contents
        .split('\n')
        .skip(2)
        .map(|package_str| {
            let amount_str = package_str
                .split(' ')
                .filter(|element| *element != "")
                .nth(1);

            if let Some(amt_str) = amount_str {
                parse_comma_separated_num(amt_str)?
                // match parse_comma_separated_num(amt_str) {
                //     Ok(amt) => Ok(amt),
                //     Err(err) => Err(From::from(err)),
                // }
            } else {
                Err(CalcPackageSizeError::InvalidInput("Input not as expected, expected the 2nd spaces-delimited item to be the size (integer)."))
            }
        })
        .sum()
}

fn main() {
    let mut wajig_input = String::from(
        "Package                           Size (KB)        Status
=================================-==========-============
geoip-database                      10,015      installed
aptitude-common                     10,099      installed
ieee-data                           10,137      installed
hplip-data                          10,195      installed
librsvg2-2                          10,412      installed
fonts-noto-color-emoji              10,610      installed",
    );
    // io::stdin().read_to_string(&mut wajig_input).expect("stdin io rarely fails.");
    match calc_all_package_size(wajig_input.as_str()) {
        Ok(total_size_in_kb) => {
            let size_in_mb = total_size_in_kb as f64 / 1024.0;
            println!("Total size of packages installed: {} MB", size_in_mb);
        }
        Err(error) => {
            println!("Oops! Encountered some error while calculating packages' size.");
            println!("Here's the error: \n {}", error);
            println!("\n-- Gracefully exiting..");
        }
    }
}

This gives a compile error:

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:59:17
   |
52 | /             if let Some(amt_str) = amount_str {
53 | |                 parse_comma_separated_num(amt_str)?
   | |                 ----------------------------------- expected because of this
54 | |                 // match parse_comma_separated_num(amt_str) {
55 | |                 //     Ok(amt) => Ok(amt),
...  |
59 | |                 Err(CalcPackageSizeError::InvalidInput("Input not as expected, expected the 2nd spaces-delimited item to be the size (integer)."))
   | |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `usize`, found enum `Result`
60 | |             }
   | |_____________- `if` and `else` have incompatible types
   |
   = note: expected type `usize`
              found enum `Result<_, CalcPackageSizeError>`
note: return type inferred to be `usize` here
  --> src/main.rs:53:17
   |
53 |                 parse_comma_separated_num(amt_str)?
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Though both errors seem semantically similar, I need to respond in different way to both the situations, so I can't make them one.

Upvotes: 3

Views: 2373

Answers (1)

Shepmaster
Shepmaster

Reputation: 430741

Use a combination of Result::map_err and Into::into:

if let Some(amt_str) = amount_str {
    parse_comma_separated_num(amt_str).map_err(Into::into)
} else {
    Err(CalcPackageSizeError::InvalidInput("Input not as expected, expected the 2nd spaces-delimited item to be the size (integer)."))
}

As Jmb points out in the comments, you could also use Ok and ? together:

if let Some(amt_str) = amount_str {
    Ok(parse_comma_separated_num(amt_str)?)
} else {
    Err(CalcPackageSizeError::InvalidInput("Input not as expected, expected the 2nd spaces-delimited item to be the size (integer)."))
}

The problem is that ? unwraps the value on success and returns from the function on failure. That means that parse_comma_separated_num(amt_str)? evaluates to a usize, as the compiler tells you:

return type inferred to be usize here

This would cause the first block to evaluate to a usize and the second block to evaluate to a Result. Those aren't the same type, resulting in the error you got.

Converting the error type using map_err preserves the value as a Result, allowing both blocks to evaluate to the same type.

See also:

Upvotes: 3

Related Questions