Option vs Results

Hi there.
The question is simple: is there any situation that can be handled with Result but not Option? Or is there something that Result can do and Option can't?
Thx.

Trivially, yes. Result represents more information than just "an error happened".

3 Likes

OK but what is this "more" in practice? and are there cases where it is mandatory to use Result ?

The None variant of Option contains no data. If you want to represent any information about an error, then you need something that allows you to put additional data in it. It doesn't matter what that information is "in practice" (or whenever).

For example, an arbitrary error message. You can't store an error message in an Option::None. Or a stack trace of the function where the error occurred.

Another example is the more advanced and more elegant way of designing errors: keeping them machine-readable by defining an enum for the possible error reasons (e.g. std::io::ErrorKind). None is always just None, you can't store an enum of reasons in it.

5 Likes

Ok, thanks, this is a good starting point

There's also a semantic difference - another developer using or maintaining your code may have different expectations based on Option vs Result.

A function doing a look up returning None clearly conveys the idea that no results were found in a way that Err(()) does not. While Result<Option<Foo>>, Error> says that the search might fail or might return no results.

8 Likes

That might be nice if it were always true, but I don't think it is. Result is sometimes also used simply when two states are returned and Option doesn't carry enough info because None has no parameter.

For example, slice::binary_search returns two states that could be represented by a specific enum:

enum BinarySearchResult {
   ExactMatch(usize), // index of match
   NotFound(usize), // index of insertion point
}

But it instead returns a Result<usize, usize> with those two values. The "not found" case could be thought of as a "failure" or just an indication that insertion is needed. It's just a lookup mismatch with extra data telling you where to insert the item if you want to add it.

I've seen people ask why an Either<T, U> type is not used for such things rather than Result<T, U>, since Either is used in some other languages. But it seems there was a decision not to add Either to the std library and just use Result instead. Perhaps because it works with the ? operator, or just to avoid another type that isn't strictly necessary.

I don't have a strong opinion about whether this is good or bad, but it is important to know that Result is not always used for "failures" as one might ordinarily think of them, at least not in the std library.

4 Likes

Binary search is the first thing that comes to mind to illustrate the utility of Result<> for things other than raising runtime errors.

match data.binary_search(n) {
    Ok(i) => {
        counts[i] += 1;
    },
    Err(i) => {
        data.insert(i, n);
        counts[i] = 1;
    }
}

The index of the insertion point of n is important to the code above whether or not the item is already in the data slice, or vector. In the first case, the count of items is increased, in the second branch the item is inserted and its count initialized.

The use of Result<> for error handling is another topic. Rust doesn't have exceptions, but Result<> along with the use of the ? operator provides very similar utility. Errors can contain useful messages that can be displayed to the user, or displayed to developers to help them debug failures.

fn assemble(p: &str, q: &str) -> Result<Record, Box<dyn Error>> {
    let a = p.parse::<f32>()?; // a and b are assigned float values;
    let b = q.parse::<f32>()?; // an error causes Err() to be returned.
    let c = a * b;
    // Imagine a lot of other code here with different types of failures.
    // For instance, maybe a database is accessed at some point to get 
    // a value.
}

In the above case Result<> either provides the needed numerical data, or an error object that states the reason for the failure. The code is very concise with the use of ? which eliminates the need to write explicit code that checks for errors at all points where one could occur. When everything goes right, the caller gets back a Record, if something goes wrong the user/developer may appreaciate the message in the Err() variant (or the value it provides).

Either is absolutely symmetric, while Result is not. In binary search, because it's, well, binary search is obvious that there's nothing found, but here is where you may insert it can be perceived as kida-sort-failure, but imagining hey, your search failed, that's where he fund the item is really hard (not impossible, probably, but I would try to avoid people with such avid imagination like a plague because who knows what would they imagine next: that a + b means divison?).

That's why Result is better than Either 99% of time, Either is only better when two cases are absolutely symmetrical and in that case I would argue that having specialized type is the best approach.

It would even, probably, be better for binary search, too, but… backward compatibility says we should stay with Result which is still better than Either in that case.

1 Like

From what I recall (and agree with) Either is not in the standard library because it is never the semantically correct thing to do. In every case one could use Either it would be better to use another enum. In many cases that enum would be Result, and in the other cases the answer is a custom enum specifically for that case. As Rust makes it very easy to construct that other enum with correct semantics for the speicific case, there is no need for Either.

4 Likes

Couldn't the same be said about tuples? In every case you use a tuple, it would be semantically more meaningful to define a struct.

A tuple is a quick generic struct just like Either is a quick generic enum.

Regarding binary_search returning Result, I think that's bad design. You often expect binary search to fall in between, it's not a failure - if you wanted exact match, you would usually use HashMap instead.

I think binary_search should return a struct containing the index and a boolean.

No. That's not true.

If you wanted the exact match then HashMap is, indeed, better. But if you expect binary_search not to find the results of then you wouldn't use binary_search either!

Because binary_search assumes that you have sorted slice you automatically get only two use modes that make sense:

  1. You are looking for something that may be there or not and don't plan to alter slice. Option would be fine here, but Result is Ok, too.
  2. You are looking for thating that may be there or not and in rare cases insert something. Resut it perfectly usable here, because it's easy to see which case if Ok and which is not Ok.

Because, realistically, the only thing that you may use Result<usize> here is to perform slow and costly insertion, which is often dozen of times slower than search.

binary_search is only useful for lots and lots and LOTS of searches with some rare insertions.

Whether that's “failure” or not is debatable (in case #1 it's failure for sure) but it's obviously exceptional case. If you you expect to hit “element not found” case often then correct structure is not sorted slice but a BTreeSet!

That doesn't match my use cases at all.

A simple use case showing you are wrong: interpolating a piecewise linear function. On a lookup I don't want to modify the function, but I do want to know which range the key falls into so that I can interpolate between the two neighbors.

Also note: in this example I don't really care if I hit a key exactly, the same interpolation formula works either way. Which shows how a struct result would work better.

If I wanted to insert into the sorted sequence, usually a better container for that is BTreeMap. If the only thing I wanted to do with the index on missed lookup is to insert, an even better approach would be not to use indices at all and use HashMap.

Good example. Now we only need to compare frequency of these two cases.

I wonder if it's possible to pull such information from crates.io …

It's not that I couldn't imagine piecewise linear function (and monotonic! otherwise binary search wouldn't work!), I just wonder how often than actually happens in language like Rust (and not in MathLab or something like that).

No, it works for any piecewise linear function. You sort the keys, not the values.

binary_search basically only makes sense if you have a static data structure but you still care about the ordering of the values between. The Result::Err index values are really the main value it provides vs a HashMap lookup.

Except I have used it precisely zero times like that in last 10 years, but used few times in the way I outlined before.

Binary search in today's word is mostly replaced with hash because CPUs are fast while RAM is slow in today's word (it was much more common when the opposite was true), but if number of elements is small then it's still faster than hash and that's how I see it in my programs.

It would be interesting to see how often these two different usecases happen in practice.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.