Is there a "more functional" way to handle both arms of a Result which call simple functions in Rust?

298 Views Asked by At

I often find myself writing code that, in human language reads like "if the method call is OK, call this function, else, call that function".

In code:

match client.favourite(&status.id) {
  Ok(_) => info!("Favourited {}", &status.id),
  Err(err) => error!("Could not favourite {}: {:#?}", &status.id, err),
};

Here, I'm merely calling a macro that logs to stdout, which could probably be abstracted away. The question is not whether this exact case can be improved, I'm looking if it is more Rust-ish to write this differently, or whether this is considered The Best Way already.

E.g. I could imagine something like:

client.favourite(&status.id)
  .on_ok(|_| info!("Favourited {}", &status.id))
  .on_err(|err| error!("Could not favourite {}: {:#?}", &status.id, err))

But I don't see such methods in the std-lib. But maybe I'm reading them wrong, maybe one of the unwrap_or_* does allow such usages?

Am I trying to do something un-rust-ish, and is the match/branch really the common way, or are there methods on Result that I'm overlooking, or usages that I am missing?

2

There are 2 best solutions below

3
Netwave On

You have map and map_err which can be used for that:

use tracing::{error, info}; // 0.1.36

fn main() {
    let status_id = 123;
    Ok("foo")
        .map(|v| {
            info!("Favourited {}", &status_id);
            v
        })
        .map_err(|err: String| {
            error!("Could not favourite {}: {:#?}", &status_id, err);
            err
        });
}

Playground

0
Finomnis On

Edit: I just realized that I re-implemented Result::map_or_else(). So use that one instead.

Although I'd still argue that the original match construct should be the way to go, because it's widely accepted and used, concise and very Rusty.


-- original answer --

I think this is considered the best way already.

Yes, you could use map() and map_err(), but they are meant for modifying the result, not for consuming it. You still have to do something with it in the end.

But If you want Result to have different functionality, you are in luck ;) Rust allows you to add functionality to an existing type.

For example, I added the resolve method to Result here, which does kind of what you want:

// Boilerplate code to get the example to work in the first place
struct Client {
    should_succeed: bool,
}

impl Client {
    fn favourite(&self, id: &mut u32) -> Result<(), String> {
        *id = 42;
        if self.should_succeed {
            Ok(())
        } else {
            Err("Things went horribly wrong.".into())
        }
    }
}

// Custom functionality for Result
trait ResolveResult<T, E> {
    fn resolve(self, on_ok: impl FnOnce(T), on_err: impl FnOnce(E));
}

impl<T, E> ResolveResult<T, E> for Result<T, E> {
    fn resolve(self, on_ok: impl FnOnce(T), on_err: impl FnOnce(E)) {
        match self {
            Ok(v) => on_ok(v),
            Err(e) => on_err(e),
        }
    }
}

fn main() {
    let mut status_id: u32 = 0;
    let client = Client {
        should_succeed: false,
    };

    client.favourite(&mut status_id).resolve(
        |()| println!("Favourited {}", &status_id),
        |err| println!("Could not favourite {}: {:#?}", &status_id, err),
    );
}
Could not favourite 42: "Things went horribly wrong."

Although I would still argue that I'd use the match construct instead. It's also four lines, it's concise, and after you get used to Rust a little, it reads just as easily.