Lifting a happy `Option` into a `Result` or continuing with another `Result`

I realized today that there is no method of Option<T> which would either lift it into a Result<T, E> by converting Some(t) to Ok(t) or return another Result<T, E> that was passed to it.

Something like

_(&self, other: Result<T, E>) -> Result<T, E> {
    match self {
        Some(t) => Ok(t),
        None => other,
    }
}

and equivalently for the case where other is a function (i.e. the *_else variants).

This would be useful in cases where you're expecting that a given option is actually a value that is present, but in case it is None, you'd just like to pass the control flow to another result.

When thinking about what name this would be given, probably the most natural choice would actually be ok_or. In fact, thinking about it, it seems that ok_or is unnecessarily restrictive by requiring you to pass the E of Result<T, E> instead of the result itself. The current behaviour of ok_or would then be recovered by simply doing x.ok_or(Err(e)) instead of x.ok_or(e).

This has the additional benefit in that it makes the ok_or of Option parallel the ok of Result more closely. In fact, such an ok_or would behave like a lift into a result (for the happy path), followed by the usual ok of Result, allowing for chaining such as:

opt.ok_or(res1).or(res2).or(res3)

Why was ok_or designed like this? Was it just an oversight? Did I miss some method?

I'm also wondering what the best way is to achieve the functionality of the last snippet in today's Rust. The best I could come up with is

opt.map_or(res1, |v| Ok(v)).or(res2).or(res3)
fn foo<T,E>(x: Option<T>, y: Result<T,E>) -> Result<T,E> {
    x.map_or(y,Ok)
}

You can pass Ok directly. Is this not short enough?

4 Likes

Right, forgot to apply eta conversion here. Thanks, it's definitely short enough, I just didn't think of it. That's related to another thing I find confusing, which is that map_or returns the unwrapped type and thus behaves unlike map (though it can be understood as a map followed by an unwrap_or).

I'm still wondering why ok_or was designed to accept the error type instead of the result, though.

I agree, the naming is a bit inconsistent IMO. The functionality provided is not too wild though.

I would think of ok_or as an into_result conversion method, which of course needs an error for the error case. Its inverse then would be the ok method on Result that can give the Option back.

The method map_or_else is pretty important because it is very general. You can define everything else consuming an Option in terms of map_or_else. It is like the higher-order-function equivalent of a match statement on an Option:

// x: Option<T>
x.map_or_else(|| { /* handle None */}, |y| { /* handle Some(y) */ })

// is equivalent to

match x {
    None => {
        // handle None
    }
    Some(y) => {
        // handle Some(y)
    }
}

A similar role is the map_or_else method on Result. The map_or method is a convenience method to avoid the need for a closure.

As I said, the naming is inconsistent. Coming from Haskell, I’m used to these kind of “allmighty”/general match-and-handle-every-case methods being called after the name of the type, which is a weird convention, too, but at least doesn’t suggest a similarity to map. (To be more specific, following this convention would mean renaming map_or_else on Option into option and map_or_else on Result into result.)

1 Like

Check out https://jethrogb.github.io/rust-combinators/ to see all the conversions that exist.

3 Likes

Has this been announced somewhere? It's the first I'm seeing it and it looks incredibly useful, even as someone who knows their way around Rust.

Thanks, all of this is very helpful for thinking about this. I'm also coming from Haskell, which is why I automatically think of functors when I see map, which is actually what threw me off about map_or* functions. Seeing them in this light makes it clearer.

2 Likes