Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I simplify this Rust code dealing with Option<T> and Result<T>?

I'm working on my first actual Rust program, a custom i3status. It's coming along nicely, but there's two places where I have to deal with Result<T> and Option<T> and the code looks really ugly, leading me to believe that I've missed some language feature or library function for writing these parts more cleanly.

This is the file in question, but I'll quote the relevant parts here. The first one is this:

fn read_number_from_file(path: &str) -> Option<i64> {
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(_) => return None,
    };
    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => {},
        Err(_) => return None,
    };
    match contents.trim().parse::<i64>() {
        Ok(val) => Some(val),
        Err(_) => None,
    }
}

Swallowing error messages here is intentional. An i3status does not have an stderr, so all I can do is skip rendering of the broken parts.

Regardless of that, this code looks ugly, with its repeated use of match to discard Err values. I tried to use the new ? operator by making the return type a std::io::Result<i64>, but str::parse returns a different type of error, so that doesn't work AFAICS. I therefore went with Option<i64> as the lowest common denominator.

The second ugly part is where this function gets used:

let energy_full = match read_number_from_file(ENERGY_FULL_PATH) {
    Some(val) => val,
    None      => return Vec::new(),
};
let energy_now = match read_number_from_file(ENERGY_NOW_PATH) {
    Some(val) => val,
    None      => return Vec::new(),
};
let is_charging = match read_number_from_file(POWER_ONLINE_PATH) {
    Some(val) => val > 0,
    None      => return Vec::new(),
};

I feel like one should be able to contract each of the match expressions into a function call like .or_else(), but .or_else(|| return Vec::new()) obviously won't work since the return is scoped to the lambda instead of the original function.

So, in both cases, my question is whether I can replace match by something more compact and idiomatic.

like image 360
Stefan Majewsky Avatar asked Dec 24 '22 12:12

Stefan Majewsky


2 Answers

To combine Option<T>s with Result<T, E>s, use ok() and the question mark operator:

fn read_number_from_file(path: &str) -> Option<i64> {
    let mut file = File::open(path).ok()?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).ok()?;
    contents.trim().parse::<i64>().ok()
}

As for the second part of the question, if you can contain all those variable bindings in a single function:

fn foo() -> Option<Vec<i64>> { // or some other Option<Vec>
    let energy_full = read_number_from_file(ENERGY_FULL_PATH)?;
    let energy_now = read_number_from_file(ENERGY_NOW_PATH)?;
    let is_charging = read_number_from_file(POWER_ONLINE_PATH)? > 0;

    Some(Vec::new()) // placeholder for the valid return value
}

You can then use unwrap_or_else() on its result to return an empty Vec upon any error:

let foo = foo().unwrap_or_else(|| vec![]);

Or just unwrap_or(), because empty vectors don't allocate memory:

let foo = foo().unwrap_or(vec![]);
like image 113
ljedrz Avatar answered Dec 26 '22 02:12

ljedrz


For the second part, you can use one of the comprehension crates available on crates.io like map_for, comp or mdo. For example with map_for:

return map_for!(
    energy_full  <- read_number_from_file(ENERGY_FULL_PATH);
    energy_now   <- read_number_from_file(ENERGY_NOW_PATH);
    power_online <- read_number_from_file(POWER_ONLINE_PATH);
    is_charging  = power_online > 0;
    => vec![ energy_full, energy_now ]
).unwrap_or_else (|| vec![]);

Full disclosure: I am the author of the map_for crate.

like image 40
Jmb Avatar answered Dec 26 '22 00:12

Jmb