pkoch
pkoch

Reputation: 2722

Mixing high-order functions and Result

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

Answers (1)

Mihir Luthra
Mihir Luthra

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);
}

Playground


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(())
}

Playground


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);
}

Playground

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);
}

Playground


Another solution could be to have something like flatten_result as suggested by Aplet in question comments.

Upvotes: 3

Related Questions