A macro that is to `Result::or_else` what `try!` is to `Result::and_then`


#1

Background

The Result::and_then method can be used to chain operations that are required to all succeed:

fn compute_b(a: A, context: &Context) -> Result<B, E> { … }
fn compute_c(b: B, context: &Context) -> Result<C, E> { … }
fn compute_d(C: C, context: &Context) -> Result<D, E> { … }

fn chain(a: B, context: &Context) -> Result<D, E> {
    let maybe_b = compute_b(a, context);
    let maybe_c = maybe_b.and_then(|b| compute_c(b, context));
    let maybe_d = maybe_c.and_then(|c| compute_d(c, context));
    maybe_d
}

However, syntax makes this a bit cumbersome. The try! macro was introduced to make this kind of thing nicer:

fn chain(a: B, context: &Context) -> Result<D, E> {
    let b = try!(compute_b(a, context));
    let c = try!(compute_c(b, context));
    let d = try!(compute_d(c, context));
    Ok(d)
}

Proposal

I’ve been using this heavily in Servo’s CSS parsing rewrite. Grammar production rules map (roughly) to functions that return Result, concatenation maps to Result::and_then or try!, and alternation maps to Result::or_else.

Still, some code would be nicer if or_else could be replaced with a macro:

macro_rules! something_something_something {
    ($e: expr) => {
        match $e {
            Ok(value) => return Ok(value),
            Err(error) => error,
        }
    }
}

Unresolved questions

  • This new macro is very general-purpose. Would it be a good candidate for inclusion in libstd?
  • What should it be named? The name “try” applies equally to “try, and return early on error” (the current try! macro, equivalent to and_then) as well as “try, and return early on success” (this new macro, equivalent to or_else). Maybe rename try! to try_ok! and name the other try_err!?

Bonus, some representative example code

// <'border-spacing'> = <length> <length>?
fn parse_border_spacing(input: &mut Parser) -> Result<(Length, Length), ()> {
    let first = try!(parse_length(input));
    let second = parse_length(input).unwrap_or(first);
    Ok((first, second))
}

// <'width'> = <length> | <percentage> | "auto"
fn parse_width(input: &mut Parser) -> Result<LengthOrPercentageOrAuto, ()> {
    something_something_something!(parse_length(input)
                                   .map(LengthOrPercentageOrAuto::Length));
    something_something_something!(parse_percentage(input)        
                                   .map(LengthOrPercentageOrAuto::Percentage));
    something_something_something!(parse_keyword(input, "auto")
                                   .map(|()| LengthOrPercentageOrAuto::Auto));
    Err(());
}

#2

An interesting idea! I think it can definitely “bake” in the cargo-verse for a while before we consider std inclusion.

Perhaps this could be named in reference to “if guards” that one sees in some functions. That is:

fn foo() {
  if a { return bar; }
  if b { return baz; }
  // normal logic, which can assume that a and b are false
}

try_err and try_ok are a bit weird to me because I don’t know which is which.

We could maybe just consider and! and or!.

fn parse_width(input: &mut Parser) -> Result<LengthOrPercentageOrAuto, ()> {
    or!(parse_length(input).map(LengthOrPercentageOrAuto::Length));
    or!(parse_percentage(input).map(LengthOrPercentageOrAuto::Percentage));
    or!(parse_keyword(input, "auto").map(|_| LengthOrPercentageOrAuto::Auto));
    Err(());
}

fn chain(a: B, context: &Context) -> Result<D, E> {
    let b = and!(compute_b(a, context));
    let c = and!(compute_c(b, context));
    let d = and!(compute_d(c, context));
    Ok(d)
}


#3

I think that all versions, try!, and! and or! are too short. I would much rather be more explicit:

fn parse_width(input: &mut Parser) -> Result<LengthOrPercentageOrAuto, ()> {
    if_ok_return!(parse_length(input).map(LengthOrPercentageOrAuto::Length));
    if_ok_return!(parse_percentage(input).map(LengthOrPercentageOrAuto::Percentage));
    if_ok_return!(parse_keyword(input, "auto")
                  .map(|_| LengthOrPercentageOrAuto::Auto));
    Err(());
}

fn chain(a: B, context: &Context) -> Result<D, E> {
    let b = if_err_return!(compute_b(a, context));
    let c = if_err_return!(compute_c(b, context));
    let d = if_err_return!(compute_d(c, context));
    Ok(d)
}

#4

Aaron points out on IRC that any change to try! should fit with https://github.com/rust-lang/rfcs/pull/243.


#5

Is this functionality not already covered by the ? operator with it’s short-circuiting?


#6

? is basically sugar for try!, essentially:

match something() { Ok(v) => v, Err(e) => return Err(e) }

The discussion here is about the opposite:

match something() { Ok(v) => return v, Err(e) => e }

#7

Since macros and functions are separate namespaces, is there a reason not to use or_else! / and_then!


#8

One option is to just add a not() method to Result, analogous to Boolean NOT in that it ‘flips’ the Result, switching Ok and Err. This behaviour could then just be try!(foo.not()), or even try!(!foo) if we implemented std::ops::Not for Result. (Also, I’ve always wanted a not method/operator on Result (and maybe Option, returning Result<(), T>) just to complete the similarities between Result and bool.)


#9

The problem with the names is they don’t read well and aren’t particularly evocative of what they do. You’d expect and_then!(x) to mean that something happens and then x happens, but it doesn’t. What you want to communicate is that you are getting the value for one arm of a control structure, with the value for the other arm being returned. If you model that with the control structure being a match, that gives you something like ok_of!(x) and err_of!(x). If you model it in terms of try/catch, you get try_of!(x) and catch_of!(x).


#10

I think that I might just favour ok! and err!.

I can see the reasoning behind adding if or return to the names, and yes, for newcomers it might be a bit easier to grasp what these macros could mean, but on the other side they will be everywhere in idiomatic Rust code and then these longer names can get quite annoying.

I think that these macros will also be one of the first things when learing about error handling in Rust, so it might be even for newcomers not that much of an issue.

They are certainly a bit more telling than try!, and I hadn’t much issues learing about try!, because it’s everywhere, so you’re just learning it once and go on.


#11

I would also really like if macros could be called without parentheses, especially macros taking only one argument could be IMHO a lot more readable:

fn chain(a: B, context: &Context) -> Result<D, E> {
    let b = ok! compute_b(a, context);
    let c = ok! compute_c(b, context);
    let d = ok! compute_d(c, context);
    Ok(d)
}

But yes I know, it might be a bit too much to ask for. :slight_smile:


#12

I would prefer the err_of!(...) and ok_of!(...) Version witch is more clear then just ok/err/try but still nicely short. Naturally something like return_on_err!(...) would be more speaking but also annoyingly long. I mean, yes longer and clearer named names are generally better but for something often used in a language it’s ok/better to have short names 'cause every one will lern that bit (as long there are not to much such bits). Also we e.g. call repeat_while_condition_is_true just while for convince, or ? :smile:


#13

Ok, I’m a bit puzzled why the of should make it more clear, because of what else than the argument of the macro should the result depend of?


#14

I put the of there because err!(...) looks the same way I would build a error construction helper macro. And the of makes it clear (at last for me) that it takes the error of the enclosed expressions result instead of using the expression to create a error.


#15

This is great! I would use this all the time.


#16

Ok, that’s a fair point, but couldn’t then the err_of! be also read as such a constructor?

At the end the user has to learn about err! or err_of!, about their meaning, and because there’s not that much point in having a constructor for Err, because it’s easy creatable by just using Err(...), I would tend to the shortest possible names.


#17

What about try_return!()? It basically means that “Evaluate this thing, and try to return it. If it fails, then continue the function”.


#18

Before it disappears into the ether, here’s a suggestion from /u/SteveMcQuark on reddit:

catch!? In a try-catch control structure, the try block is the optimistic code path, while the catch block is the on-error code path. With the try! macro, the rest of the function is the optimistic code path. So, analogously, catch! would be the name of a macro that makes the rest of the function the on-error code path. The macros essential represent an inversion of their respective control structures.

I’m not going either way on this argument, but I find it pretty interesting.


#19

Thanks for all the feedback. I went with return_if_ok!, which I feel describes best what the macro actually does. By this logic, the try! macro could be named return_if_err!, but that doesn’t matter much as it will likely be replaced with a ? operator.

I’ve published a small crate with only the return_if_ok! macro on GitHub and on crates.io. Maybe it could be added to libstd later, in the meantime it can be used with just #[macro_use] extern crate return_if_ok;.

That’s not the same. try!(Ok(4).not()) would expand/simplify to return Err(4), while I’d want return Ok(4).

The same reasoning applies just as well to both return_if_err! (a.k.a. try! or ?) and return_if_ok!. This would not help telling them apart.

Although they happen to share a name, the try! macro does something very different from the try part of a try-catch structure, so I don’t think that catch! works for the name of this macro.


#20

Personally, I’d be tempted to go with “recover!” - “I tried to do X and Y, but when it failed I recovered with Z.”

EDIT: Oh, no, I was thinking of unwrap_or_else. This is more “persevere!” than “recover!”