Reputation: 2722
I'm not really sure how to write a high-order function that wants to return an error of its own and make it look nice when I use it on another functions that also return a Result
.
This is hard to describe. Here's an example:
use std::fs;
use std::io;
use std::io::Read;
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct MyError;
impl Error for MyError {}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyError")
}
}
fn log_something() -> Result<(), MyError> {
unimplemented!()
}
fn log_and_call<T>(f: &dyn Fn() -> T) -> Result<T, MyError> {
log_something()?;
Ok(f())
}
fn the_answer() -> i32 {
42
}
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = fs::File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let _logged_answer: Result<i32, MyError> = log_and_call(&the_answer);
let _logged_username: Result<Result<String, io::Error>, MyError> = log_and_call(&read_username_from_file);
}
How do I avoid having that Result<Result<String, io::Error>, MyError>
there? I'd like to have Result<String, Error>
, or maybe something even more sensible.
Maybe there's a more idomatic way to do this?
Upvotes: 4
Views: 104
Reputation: 6779
You can do this with the help of traits:
fn log_and_call<T: ToResult>(f: &dyn Fn() -> T) -> Result<T::Value, MyError> {
log_something()?;
f().to_result()
}
fn the_answer() -> i32 {
42
}
trait ToResult {
type Value;
fn to_result(self) -> Result<Self::Value, MyError>;
}
impl<T, E: Error> ToResult for Result<T, E> {
type Value = T;
fn to_result(self) -> Result<Self::Value, MyError> {
self.map_err(|err| MyError::from(err))
}
}
impl ToResult for i32 {
type Value = i32;
fn to_result(self) -> Result<Self::Value, MyError> {
Ok(self)
}
}
fn main() {
let _logged_answer: Result<i32, MyError> = log_and_call(&the_answer);
let _logged_username: Result<String, MyError> = log_and_call(&read_username_from_file);
}
I don't exactly know your use case, so if you just want to grab the value inside the result wherever you are calling, Box<dyn std::error::Error>
can be used at return position:
fn main() -> Result<(), Box<dyn Error>> {
let _logged_answer: i32 = log_and_call(&the_answer)?;
let _logged_username: String = log_and_call(&read_username_from_file)??;
Ok(())
}
Another way could be to change log_and_call
to this:
fn log_and_call<T, E: Error>(f: &dyn Fn() -> Result<T, E>) -> Result<T, MyError> where MyError: From<E> {
log_something()?;
Ok(f()?)
}
and then calling the function like:
fn main() {
let _logged_answer: Result<i32, MyError> = log_and_call(&||-> Result<_, MyError> {Ok(the_answer())});
let _logged_username: Result<String, MyError> = log_and_call(&read_username_from_file);
}
You could define a macro or a function to do this wrapping in a closure returning Result
:
macro_rules! wrap {
($($content:tt)+) => {
||-> Result<_, MyError> {Ok($($content)+)}
};
}
fn main() {
let _logged_answer: Result<i32, MyError> = log_and_call(&wrap!(the_answer()));
let _logged_username: Result<String, MyError> = log_and_call(&read_username_from_file);
}
Another solution could be to have something like flatten_result
as suggested by Aplet in question comments.
Upvotes: 3