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.
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![]);
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With