Process `Option<Result<A, B>>`


#1

I have recently started to learn a bit of Rust, and coming from Scala I have discovered a lot of similarities in syntax and concepts. I played a bit with Rust and learned that when iterating over the lines of a file I need to deal with a Option<std::io::Result<String>>. I understand that an iterator can produce a value or not, and that when producing that value an error can occur, but I am wondering why there is no thing such as Box from the Scala Lift framework (https://app.assembla.com/wiki/show/liftweb/Box) that can be either Full(value), Empty or Failure(err). Wouldn’t that be a data structure much easier to process with map(), match etc. than an Option<Result<A, B>>? Is there a straightforward way to write "if None, return None, if Error return Error, if normal value x return f(x)"?


#2

Well, AFAIK it’s because of how the rest of the Rust standard library is written; it separates responsibility more cleanly this way. The Option<T> part allows for all the Iterator logic to work without caring about errors (because not all iterators can generate errors), and the Result<A, B> part lets error checking logic, like the try! macro and the ? operator without having to think about iterators.

For that last bit, you could probably do something like

for i in iterator {
    let x = i?;
    // Your code goes here
}

Which would iterate through the iterator while returning with an error if the iterator returns an error


#3

if None, return None, if Error return Error, if normal value x return f(x)

fn process(o: Option<Result<A, B>>) -> Option<Result<C, B>> {
    o.map(|r| r.map(f)) // f: fn(A) -> C
}

#4

I’ve been annoyed by Option<Result<>> a few times too. The problem is that sometimes in Rust Option<Result<T, E>> is used as it was sematically Result<Option<T>, E>. In the iterator case, it’s made that way so it still fits in the iterator API and that way all the functions that work on iterators don’t have to know that there’s a Result somewhere inside.

I think that a method that just converts Option<Result<T, E>> to Result<Option<T>, E> would be a good addition to std. For example, I would like to write this code without a match statement:

// opt: Option<u8>
// process: fn(u8) -> io::Result<Struct>

let foo = match opt {
    None => None,
    Some(x) => process(x)?,
};

I’d like it to be (I don’t have a good idea how to name this method):

let foo = opt.map(process).to_result_of_option()?; 

or maybe even:

let foo = opt.map(process)?;

#5

But that is exactly why I was asking: Instead of having Option<Result<T, E>> or Result<Option<T>, E>, why not have a three-valued enum that describes the three combinations “succeeded with result”, “succeeded without result” and “error” within a single structure? (Ever since I came across Lift’s Box I wonder why not everyone adopted that pattern.)


#6

The main reason to have an Option<Result<T, E>> is to be able to handle the optional part and the may-fail part semantically different, as @cactorium describes. If you want to handle it all, you can:

match optional_result() {
    Some(Ok(x)) => got_value(x),
    Some(Err(e)) => got_error(e),
    None => got_nothing(),
}

#7
  • Option<…> comes from the way Iterators work (either there’s an element, or there are no more elements). The Iterator interface itself is unconcerned about what’s inside.
  • Result<…> comes from the way IO works (either succeeds or fails due to I/O errors).

They are from two disparate parts of the standard library, wholly unaware of each other. It’s only when you put both together you end up with the Option<Result<…>> situation. So the fact that you get this weird type is not because it was designed or intended as such, but just a natural consequence of what happens if you combine these two things together.


#8

To build on Fylwind’s (and, for that matter, cactorium’s) point a bit, the only way in Rust to have sequences where elements can represent an error would be for those sequences to have an entirely separate family of Iterator types, whose next() methods return a member of a three-value enumeration. There’s no way, given Rust’s type system, for next() to return either Some(T) or Empty when T is a non-Result type, and either Some(T), Err(E), or None when T would otherwise be Result.

Having two separate iterator types means two ways to desugar a for loop depending on the type of the sequence (ick), two ways to implement try!(), and so on.

That’s not to say that it can’t be done, but I think, if you want it, you may have to implement a proof of concept yourself first, to explore where the benefits are and where the dragons lurk.