[SOLVED] Wrapper around `Result` that can return both the error and the value?

I am currently writting a wrapper for a C library, which follows the following convention for all of its functions:

ErrorCode functionName(arg1, arg2, ..., *ptrResult);

The developer is expected to allocate the memory in which the result will be written and pass it through *ptrResult. The return value indicates if the operation was successful or not.

I am wrapping all functions so they become, in Rust:

fn function_name(arg1, arg2, ...) -> Result<T, ErrorCode>;

This works most of the time, because usually if the returned code is not "success", no result is written on *ptrResult (in other words we usually don't care about the code AND the result).

But there are a few cases where the function can return a code like "something went wrong but not badly enough to make the call fail". In this case, the error code is not "success", but a value is still written on *ptrResult. So in this case I want to retrieve both the code and the result.

The thing is that 95% of the time, the Result pattern works well, so I would like to keep that. Basically I would like to make a structure that can be converted transparently to Result, and on which I would add a destructure method, so both these snippets are valid:

// Use the expect() method implemented by Result
let value = some_function().expect("Something went wrong");

// Use the custom destructure() method to match on both the code and the result
let value = match some_function().destructure() {
    (ErrorCode::Success, value) => value,
    (ErrorCode::PartialSuccess, value) => some_processing(value),
    (_, _) => panic!("Something went wrong")
}

Here is the list of solutions I thought about, and why it doesn't work:

  • implement Into<Result<T, ErrorCode>> for MyResult<T>: requires the developer to manually call into() after each call, making the API tedious to use
  • implement Deref<Target = Result<T, ErrorCode>>: doesn't work because deref retrieves a reference, while I would like to retrieve a value (that would be consumed by expect() or destructure() for example)
  • the last chance solution would be to manually re-implement all the methods and traits of Result, but it seems to be way too overkill for what I'm trying to do

I was thinking maybe the macros could help in some way, but I cannot put my finger on how (I must say I have very little experience with macros).

Do you guys have any advice to share on the issue?

1 Like

I think I would put that kind of structure into the error type. E.g.

enum MyError {
    Total(ErrorCode),
    Partial(ErrorCode, ValueType),
}

and use that inside the Result.

1 Like

This seems like a bad idea. Result<T,E> has the semantics that it either succeed and returned T or failed and returned E. You want something that is any of the following 3:

  • Success
  • Partial Success w/ Partial Failure Reason
  • Failure

This is different semantics. One thing you could do would be to make your T in your Result<T,E> be a PartialResult<PT,PE>. So, for things that could partially succeed/fail you would return, Result<PartialResult<PT,PE>,E> and for things that could only fully succeed or full fail you would return Result<T,E>. The type PartialResult would simply have 2 fields. 1 for PT and 1 for PE which would both always be populated, so no reason to re-implement Result-like logic on it.

EDIT: @Phaylon has the better idea. Similar, but, better.

If you have multiple levels of errors (critical and non-critical), it makes sense to separate them to different levels of your type structure. Here is a basic example:

struct Data {
    value: T,
    needs_processing: bool,
}
fn func() -> Result<Data, CriticalError> { unimplemented!() }
fn main() {
    let data = func().unwrap();
    let _value = if data.needs_processing {
        process(data.value)        
    } else {
        data.value
    };
}

If you need more information about a non-critical error, you can replace needs_processing field with something like error: Option<NonCriticalError> or even extra_data: Result<SomeMoreOutput, NonCriticalError> (so you'll have a Result inside another Result), but it depends on what error data you want to handle.

Ok so it's clear that the consensus it that the proper way to do what I'm trying to do is to introduce an additional nesting level in either the value or the error field, depending on what is the most relevant to the function.

Thank you very much for your answers!