Skip to content

Add Option::try_or_else  #59

Closed as not planned
Closed as not planned
@cgwalters

Description

@cgwalters

Proposal

Add Option::try_or_else.

(note: This was formerly proposed as Option::result_or_else that only handled Result)

Problem statement

This generalizes Option::or_else to also handle Result, which is very useful in cases where we want to convert an Option<T> to a T, but use a fallible (Result<T>) fallback path in a combinator chain.

Motivation, use-cases

    // Our program needs a string for something (e.g. filename, database table name, etc).
    // To start, we can get this as an optional string - here a CLI argument, but it could be anything; e.g.
    // a value parsed from JSON, etc.  The `.nth(1)` call here gives us an `Option<String>`.
    // We want to compute a fallback value, but doing so can fail (e.g. return `Result<String>`).
    // Here's the trick - we use `.map(Ok)` to convert our `Option<String>` to `Option<Result<String>>`,
    // which means the type signature works for `.unwrap_or_else()`, giving us in the end a Result<String>,
    // on which we can use the regular `?` operator.
    let v: String = std::env::args()
        .nth(1)
        .map(Ok)
        .unwrap_or_else(|| std::fs::read_to_string("/etc/someconfig.conf"))?
;

This could instead be:

    let v: String = std::env::args()
        .nth(1)
        .try_or_else(|| std::fs::read_to_string("/etc/someconfig.conf"))?;

This convenience method is particularly useful when chaining further combinators, e.g.:

    let v: String = std::env::args()
        .nth(1)
        .try_or_else(|| std::fs::read_to_string("/etc/someconfig.conf"))
        .map(|v| v.trim().to_string())?;

Solution sketches

index 28ea45ed235..05b48728ae3 100644
--- a/library/core/src/option.rs
+++ b/library/core/src/option.rs
@@ -1346,7 +1346,8 @@ pub const fn or(self, optb: Option<T>) -> Option<T>
     }
 
     /// Returns the option if it contains a value, otherwise calls `f` and
-    /// returns the result.
+    /// returns the result.   See also [`try_or_else`] which can also handle
+    /// [`Result`].
     ///
     /// # Examples
     ///
@@ -1372,6 +1373,35 @@ pub const fn or_else<F>(self, f: F) -> Option<T>
         }
     }
 
+    /// Returns the option if it contains a value, otherwise calls `f` and
+    /// returns the result.  This is a more general version of [`or_else`]
+    /// that can also handle [`Result`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// fn nobody() -> Option<&'static str> { None }
+    /// fn vikings() -> Option<&'static str> { Some("vikings") }
+    /// fn success() -> Result<&'static str> { Ok("vikings") }
+    /// fn barbarians() -> Result<&'static str> {  Err(io::Error::new(io::ErrorKind::Other, "oh no!")) }
+    ///
+    /// assert_eq!(Some("barbarians").try_or_else(vikings), Some("barbarians"));
+    /// assert_eq!(None.try_or_else(vikings), Some("vikings"));
+    /// assert_eq!(None.try_or_else(nobody), None);
+    /// assert_eq!(None.try_or_else(success).unwrap(), "vikings");
+    /// assert_eq!(None.try_or_else(barbarians).is_err());
+    /// ```
+    #[inline]
+    #[unstable(feature = "try_or_else", issue = "1")]
+    #[rustc_const_unstable(feature = "try_or_else", issue = "1")]
+    pub const fn try_or_else<R: ops::Try<Output = T>, F: FnOnce() -> R>(self, f: F) -> R {
+        if let Some(v) = self {
+            ops::Try::from_output(v)
+        } else {
+            f()
+        }
+    }
+
     /// Returns [`Some`] if exactly one of `self`, `optb` is [`Some`], otherwise returns [`None`].
     ///
     /// # Examples

Links and related work

I discovered/made up this idiom of using map(Ok).unwrap_or_else() while working on a project, and it made some code much more elegant versus what I was doing before with multiple-line match statements.

I then did some looking around via code search engines:

And there's quite a lot of usage of this. Enough, I think that there's an argument for promoting this pattern to std.

I was chatting with @cuviper about this and he mentioned:

"another way to write this is option.ok_or(()).or_else(|()| new_result)?"

I did some further searching, and there's also some code using this variant, though less:

Notably the first link shows a hit in cargo.

Later, several people pointed out in discussion that by using the Try trait, we can effectively generalize the current Option::or_else to handle both Option and Result for the callback.

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions