story
may_fail_who_knows()
.map(use_value)
.map_err(some_error_processing)
.and_then(another_computation_which_can_fail)
.or_else(with_some_error_handling_that_can_rescue)
.unwrap_or(a_default_value)
Basically, instead of nested match expressions, you get a "pipeline". let x = match may_fail_who_knows() {
Ok(y) => Ok(another_computation_which_can_fail(use_value(x))),
Err(e) => with_some_error_handling_that_can_rescue(
some_error_processing(e)),
};
match x {
Ok(y) => y,
_ => a_default_value,
}
It's a bit more verbose than using the combinators, but someone coming across it for the first time will understand it immediately because there's less to remember to understand it (this is where go really shines).Also: by avoiding functors there are fewer subtle lifetime issues and `move ||` stuff to deal with and you can return from the containing function and use the `?` operator.
During the discussions of how `.await` was going to work for rust async there was the proposal to add other suffix keywords. So this would look like:
may_fail_who_knows()
.match {
Ok(y) => Ok(another_computation_which_can_fail(use_value(x))),
Err(e) => with_some_error_handling_that_can_rescue(
some_error_processing(e)),
}
.match {
Ok(y) => y,
_ => a_default_value,
}
Maybe not that different.You just had to write the concrete types (Ok and Err) out. What if these types are changed later on, e.g. to "Some(...)" and "None" or "Ok" and "ManyErrs(...)"?
As you said, it is easier to understand. Because it less abstract. This can be a good thing, but as well be a bad thing - but one thing is sure: while it does the same in the concrete case, the code is not "equivalent" when it comes to refactoring and certain changes.
Yes, there are a very specific and limited set of changes you could make to the types here and not have to change this code. You can't replace `Result<>` with `Option<>` because of `map_err`. You could replace `Result<>` with something else that is very `Result<>`y, but your flexibility would be very limited if you didn't want to change the signatures of `with_some_error_handling_that_can_rescue` or `some_error_processing`.
I'm sure it's possible to contrive an example where this would help, but I don't believe that it would be that much of a help very often in practice. I think it's just a bit more monady and people who take the time to learn monads then want to apply that wherever they can.
I'm not saying that the combinators should never be used, but that each additional one you use increases the cognitive burden of reading your code. So the question becomes: which of the combinators are worth it.
I would argue that `.map_err()` is useful as it compliments the `?` operator. Hopefully with (and often without) `try!` blocks many of the other ones can go away. In particular I think that language constructs are almost always better than `.and_then()`.
match may_fail_who_knows() {
Ok(success) => {
do_something_with_success(success)?
},
Err(failed) => {
some_error_processing(failed)
}
}
As `do_something_with_success` in a closure can't early return from the function (since it's in a closure), which makes sense, but just annoying to read nested results. let result = may_fail_who_knows()?
.use_value_which_might_also_fail()?
.use_a_different_way_that_might_fail()?;