15

I have this code.

if let Ok(file) = env::var("CONF") {
    if let Ok(mut reader) = fs::File::open(&file) {
        if let Ok(conf) = Json::from_reader(&mut reader) {
            // do something with conf
        }
    }
}

I'm trying to make it less like a festive holiday tree and was thinking about chaining. Notice that each step in this chain produces another Result, so clearly this won't work (we get Result in Result).

let conf = env::var("CONF")
    .map(fs::File::open)
    .map(Json::from_reader);

// do something with conf

Also my error types differ for each step, which means I can't just replace .map with .and_then.

I think I'm looking for something that is similar to JavaScript's promises. That is, a promise returned from inside a promise unwraps the inner promise. The signature should probably be along the lines of:

impl<T, E> Result<T, E> {
    fn map_unwrap<F, U, D>(&self, op: F) -> Result<U, D>
        where F: FnOnce(T) -> Result<U, D>
}

Is there such a mechanism in Rust? Is there another way to get rid of my festive holiday tree?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Martin Algesten
  • 13,052
  • 4
  • 54
  • 77

3 Answers3

16

Is there such a mechanism in Rust?

Yes — although not all in one shot like you've presented. Let's review your theoretical signature:

impl<T, E> Result<T, E> {
    fn map_unwrap<F, U, D>(&self, op: F) -> Result<U, D>
    where
        F: FnOnce(T) -> Result<U, D>,
    {}
}

This cannot work - assume that we start with an Err variant - how would this code know how to convert from E to D? Additionally, &self isn't appropriate for functions that want to convert types; those usually take self.

There are two components that you will need to combine:

  1. Result::and_then

    impl<T, E> Result<T, E> {
        fn and_then<U, F>(self, op: F) -> Result<U, E>
        where
            F: FnOnce(T) -> Result<U, E>,
        {}
    }
    
  2. Result::map_err

    impl<T, E> Result<T, E> {
        fn map_err<F, O>(self, op: O) -> Result<T, F>
        where
            O: FnOnce(E) -> F,
        {}
    }
    

Then you will need a type that can represent both error types. I'll be lazy and use Box<Error>

Combined together, you need something like:

use std::env;
use std::fs::File;
use std::error::Error;

fn main() {
    let conf = env::var("CONF")
        .map_err(|e| Box::new(e) as Box<Error>)
        .and_then(|f| File::open(f).map_err(|e| Box::new(e) as Box<Error>));
}

Now each call converts the error value to a shared type, and the result is chainable with and_then. Presumably, your real code would create an error type that is suited to your problem, and then you would use that in the map_err call. I'd implement From, then you can have just:

let conf: Result<_, Box<Error>> = env::var("CONF")
    .map_err(Into::into)
    .and_then(|f| File::open(f).map_err(Into::into));
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • `Into::into` is a nice touch. Is there a "free" function like that to get a ref? `String` -> `&String`? – Martin Algesten Dec 14 '16 at 17:30
  • @MartinAlgesten I think you are looking for [`AsRef::as_ref`](https://doc.rust-lang.org/std/convert/trait.AsRef.html), but it may not work in the case you want. You can't map an `Option` to `Option<&str>`, for example, because it would be taking a reference to something that would then be dropped. There is `Option::as_ref` though. – Shepmaster Dec 14 '16 at 18:45
5

If you actually want to ignore the results as you are doing with if let you can use a macro like this:

macro_rules! iflet {
    ([$p:pat = $e:expr] $($rest:tt)*) => {
        if let $p = $e {
            iflet!($($rest)*);
        }
    };
    ($b:block) => {
        $b
    };
}


fn main() {
    iflet!([Ok(file) = env::var("CONF")]
           [Ok(mut reader) = File::open(&file)]
           [Ok(conf) = Json::from_reader(&mut reader)] {
        // do something with conf
    });
}

Playground (without Json part)

The macro is originally from an answer I made to a similar question on Options, but it works with any if let. Though, with Result you often want to use the Err in some way, so I would usually lean towards the approach explained by Shepmaster or ?/try!.

Community
  • 1
  • 1
Erik Vesteraas
  • 4,675
  • 2
  • 24
  • 37
1

The anyhow crate makes it possible to chain different error types together, and combined with the ? operator you can express sequences of Results very concisely. The downside is you need to return a Result from the function, so in certain cases you'll need to create a helper. If you only need the helper in one place though you can define it inside the calling function so it doesn't pollute the surrounding namespace.

Here's a simple example where I wanted to get a usize from an environment variable:

fn main() {
    fn env_int(key: &str) -> anyhow::Result<usize> {
        Ok(std::env::var(key)?.parse()?)
    }

    println!("{:?}", env_int("MISSING"));
    println!("{:?}", env_int("CARGO_PKG_VERSION"));
    println!("{:?}", env_int("PLAYGROUND_EDITION"));
}
dimo414
  • 47,227
  • 18
  • 148
  • 244