4

Is there something similar to the ? shortcut which instead of returning a result from a function when there is an error, returns a predefined value?

Basically I want to know if it is possible to do the following in one line:

fn index() -> String {
    let temp = some_func("pass"); // some_func returns a Result 
    if temp.is_err() {
        return "No parameters named pass".to_string();
    }
    try_decrypt_data(temp.unwrap())
}

I tried using unwrap_or_else(), but that just returns the closure not the outer function. i.e.

try_decrypt_data(params.get("pass").unwrap_or_else(|| return "No parameters named pass".to_string(); )) // This doesn't work
Boiethios
  • 38,438
  • 19
  • 134
  • 183
8176135
  • 3,755
  • 3
  • 19
  • 42
  • 2
    Just to be sure: `try_decrypt_data` returns the decrypted data as `String`? And if an error occurs you want to return the string `"No parameters named pass"`, yes? So what if the decrypted data is exactly `"No parameters named pass"`? How would you be able to tell if an error occurred or if that's just what the decrypted data is? – Lukas Kalbertodt Apr 13 '18 at 09:24
  • @Lukas Kalbertodt The actual try_decrypt_data returns a Result, which I then unwrap with `unwrap_or("...").to_string()`, but that had nothing to do with the question, so I left it out here to keep it simple. – 8176135 Apr 13 '18 at 09:44

3 Answers3

5

This is kind of possible, but usually not a good idea, especially not in your example (I will explain that later).

You cannot easily return a String and make ? return a default value, but you can define your own string type and implement std::ops::Try for it. Note that Try is still unstable!

Let's see how this would work:

// Just wrap a string
struct StringlyResult {
    s: String,
}

// Convenience conversion 
impl From<String> for StringlyResult {
    fn from(s: String) -> Self {
        Self { s }
    }
}

// The impl that allows us to use the ? operator
impl std::ops::Try for StringlyResult {
    type Ok = String;
    type Error = String;

    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.s == "No parameters named pass" {
            Err(self.s)
        } else {
            Ok(self.s)
        }
    }

    fn from_error(s: Self::Error) -> Self {
        if s != "No parameters named pass" {
            panic!("wat");
        }
        Self { s }
    }

    fn from_ok(s: Self::Ok) -> Self {
        if s == "No parameters named pass" {
            panic!("wat");
        }
        Self { s } 
    }
}

With that we can implement index() like this:

fn index() -> StringlyResult {
    let temp = some_func("pass")
        .map_err(|_| "No parameters named pass")?; 
    try_decrypt_data(&temp).into()
}

(Complete code on the Playground)

So yes, the Try trait enables users to use the ? operator with their own types.


However, as presented in your example, this is a terrible idea. You probably already noticed the "wat" sections in my code above. The problem is that your OK-type already exhausts the whole type (all instances of the type are valid OK-instances).

Consider a function get_file_size() -> u64. Now this function can fail (i.e. it cannot determine the file size). You couldn't just return 0 to signal a failure occurred. How would the caller of your function distinguish between an environment in which the function cannot determine the file size and an environment where there the file is actually 0 bytes large?

Similarly, how would the caller of your function distinguish the situation in which an error occurred and the situation in which the decrypted text is literally "No parameters named pass"? The caller can't! And that's bad.

Notice that there is something similar, which is not as bad, but still not really idiomatic in Rust: get_file_size() -> i64. Here, we could return -1 to signal a failure. And this is less bad because -1 can never be a valid file size! (in other words, not all instances of your type are valid OK-instances). However, in this case it is still super easy to forget to check for errors. That's why in Rust, we always want to use Result.


To make error handling easier, consider using the crate failure. With that, you can easily use strings as error messages without sacrificing type safety or sanity of your program. Example:

use failure::{Error, ResultExt};

fn index() -> Result<String, Error> {
    let temp = some_func("pass")
        .context("No parameters named pass")?; 
    Ok(try_decrypt_data(&temp)?)
}
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Thanks, this answers the question, but seems like a lot of work to reduce 2 lines of code :D. While in this case it is just a personal project where I know what decrypt the output will look like, in future I think I will return a result and have a wrapper function actually returning strings to the client. – 8176135 Apr 14 '18 at 00:02
3

I would create an inner function that uses a Result. This allows you to use the question-mark operator for the various error messages / default values that you'd like to return. You can then call the inner function and take either the success value or error value:

fn index() -> String {
    fn inner() -> Result<String, String> {
        let t1 = some_func("pass").map_err(|_| "No parameters named pass")?;
        let t2 = some_func2(t1).map_err(|_| "A second error")?;
        let t3 = some_func3(t2).map_err(|_| "A third error")?;
        Ok(t3)
    }

    match inner() {
        Ok(v) => v,
        Err(v) => v,
    }
}

There's an unstable feature called try blocks that promises to make this slightly less bulky:

#![feature(try_blocks)]

fn index() -> String {
    let v = try {
        let t1 = some_func("pass").map_err(|_| "No parameters named pass")?;
        let t2 = some_func2(t1).map_err(|_| "A second error")?;
        some_func3(t2).map_err(|_| "A third error")?
    };

    match v {
        Ok(v) => v,
        Err(v) => v,
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
1

There is no such utility, but you can always write a macro:

macro_rules! return_if_err {
    ( $to_test:expr, $default:expr ) => (
        if $to_test.is_err() {
            return $default;
        }
    )
}

fn pop_or_default(mut v: Vec<i32>) -> i32 {
    let result = v.pop();
    return_if_err!(result.ok_or(()), 123);

    result.unwrap()
}

fn main() {
    assert_eq!(pop_or_default(vec![]), 123);
}

You cannot return from an outer scope from a closure.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • [Using a `match`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=88d3b266caf1e9bc67ca8845d3586822) would avoid the `unwrap`. – Shepmaster Nov 20 '19 at 21:12
  • [Another example](https://stackoverflow.com/a/51345372/155423) of how such a macro could be written. – Shepmaster Nov 20 '19 at 21:56