2

tl;dr Is it possible to extend std::result::Result to add my own variant that signals "things are Okay but also..." and keep impl Result methods like is_ok()?


I want to extend Result to signal additional states that a function caller can use for special cases.

use std::result::Result
use std::io::Error;

/// Extend Result to also signal "things are okay but check on things"
enum ResultExt<T, E> {
    Result<T, E>,
    OkButCheckThings(T),
}

pub fn do_stuff() -> ResultExt<u64, Error> {
  // ...
}

pub fn main() -> {
  let var = match do_stuff() {
    Ok(val) => { val },
    Err(err) => { 0 },
    OkButCheckThings(val) => { check_things(); val },
  }
  dbg!(var);
}

It's possible to plainly extend an Enum. But I would also like to use the underlying Result<T, E> functions like is_ok.

let var2 = do_stuff();
if var2.is_ok() {
   println!("It is totally Ok, nothing to check!");
}

I created a rust playground example that successfully extends Result<T, E> but the extended enum cannot use functions like is_ok().



The real-world use-case is a function that calls std::io::Read may need to "modify" the returned Result to signal additional states beyond Ok and Err. But I want these various "meta states" to be captured by one enum, as opposed to returning various other bool flags (I want to avoid return signature with (Result<T>, bool, bool). This would allow one clean match statement of all possible states; Ok, Err, "Okay but...", "Err but ...", etc..

JamesThomasMoon
  • 6,169
  • 7
  • 37
  • 63
  • 4
    Seems that what you want is a `Result` – Netwave Sep 17 '21 at 07:19
  • Hello, this can help? https://stackoverflow.com/questions/25214064/can-i-extend-an-enum-with-additional-values – Zeppi Sep 17 '21 at 07:29
  • I mentioned https://stackoverflow.com/questions/25214064/can-i-extend-an-enum-with-additional-values in the Question. It's different than this Question. I want to extend an `Enum` **_and still use `impl ...` functions_**. – JamesThomasMoon Sep 17 '21 at 09:14
  • 1
    You wouldn't want that because the method wouldn't be correct anymore. Eg. `is_ok` is implemented by checking the `Ok` case, while `is_err` is implemented by returning `!self.is_ok()`. So if you could extend `Result` and “inherit its methods” you'd end-up with incorrect methods because `is_err` would return true for your new non-error case. Sure, `is_err` could be implemented some other way to make it work, but the point is that there is no way that extending `enum`s would make sense for all methods. Another simple example: What should an inherited `transpose` do on an extended `Result`? – mcarton Sep 17 '21 at 15:23

2 Answers2

3

There is no current way of "extending" and enum perse. But it could be simply solved by embedding your own enum type into the result itself.

Simple example, similar to yours:

use std::fmt::Display;

enum StuffToCheck<T> {
    Ok(T),
    CheckThis(T),
}

impl<T> StuffToCheck<T>
where
    T: Display + Copy,
{
    pub fn check_things(&self) -> T {
        match self {
            Self::Ok(val) => {
                *val
            }
            Self::CheckThis(val) => {
                println!("Checking stuff for {}", val);
                *val
            }
        }
    }
}

fn do_stuff() -> ResultExt<u64> {
    Ok(StuffToCheck::CheckThis(10))
}

type ResultExt<T> = Result<StuffToCheck<T>, std::io::Error>;

fn main() {
    let var = match do_stuff() {
        Ok(result) => result.check_things(),
        Err(_err) => 0,
    };
    dbg!(var);
}

Playground

You could even use nested pattern matching:

...
match do_stuff() {
    Err(e) => {//handle error}
    Ok(StuffToCheck::Ok(value)) => { value },
    Ok(StuffToCheck::CheckThis(value)) => {
        check_things(value);
        value
    }
}
...
Netwave
  • 40,134
  • 6
  • 50
  • 93
1

I think this is an instance of the X-Y problem. You can use the built-in result, you just need a different error type, that returns an option: Some(partial_result) or None.

For example you have function parse, that can attempt to adjust for a malformed input, but report the error.

pub fn parse(b: &str) -> Result<&str, CustomParseError> {
    // Do something that might fail, 
    if failed(){
       return CustomParseError::new(None)
    } else if partially_failed() {
       return CustomParseError::new(Some(partial_result))
    } else {
        return completeResult
    }

}

This way you have a clean code path where nothing failed, and all of your assumptions are correct, and if it's not => instead of unwrapping, you match and check which case you have. This is vastly superior, because the error often contains enough information for you to reconstruct both what went wrong, and what could be done to fix it.

Alex Petrosyan
  • 473
  • 2
  • 20