Adding a way of getting the value of Result<T, T> without match statement

Hey,

Was writing some rust application code and found myself writing methods with signatures that look something like fn(self, some_db_pool_thing) -> Result<Self, Self>.

I'm doing this so that if I try to update the database, the current self value isn't valid anymore so that I must re-set the value if I want to use it again.

Maybe I should just be using fn(&mut self, some_db_pool_thing) -> bool but then if I pull data from the database I would need to always remember to reassign the value at the end of the function and also handle returning maybe a boolean flag? My approach feels much more elegant to me and I would really appreciate some impl for Result<T, E> where T == E to just get the inner value.

The ideal approach for me would look something like

fn main() {

    // Woah, what a cool `Foo()` instance!
    let mut my_foo = Foo(25);

    my_foo = my_foo.update_foo_and_db(20).either(); // Lets go update it
    my_foo = my_foo.update_foo_and_db(501).either(); // Lets go update it again
    // Here we don't care if we caused an error, if we did we would use a match statement.
    // The main thing is we can't use my_foo again if we don't re set it from the callers point of view
    // preventing accidentally using old data.

}

#[derive(Debug)]
struct Foo(i64);

impl Foo {
    pub fn update_foo_and_db(self, new_foo_value: i64) -> Result<Self, Self> {

        // emulating not being able to update outside source
        if new_foo_value > 500 {
            return Err(self);
        }

        return Ok(Self(new_foo_value));

    }
}

I really like the name .either() as I feel it fits with .ok() or .err() without implying it returns an Option<T>.

Hearing someone else's thoughts would be appreciated.

Why not use (Self, Result<(), E>) instead? If you don't like it for some reason, then you can use pattern matching like this:

let (Ok(my_foo) | Err(my_foo)) = my_foo.update_foo_and_db(20);

Also, I think that a &mut self method would be more idiomatic here. You can still return Result instead of bool to remind about potential failure case.

3 Likes

I wouldn't mind a method,[1] but there currently exists ways that don't use match.

result.unwrap_or_else(std::convert::identity)

  1. e.g. this comes up with binary_search ↩ī¸Ž

7 Likes

I think I might go with (Self, Result<(), E>) just because it does basically what I need for now (or more likely (Self, bool)). Still though if I had an option the either() method would be my choice. While you still accomplish the same goal, there is just something nice and explicit about Result<T, T> with .either() and I do think this would be a nice addition.

edit: Also I may just be ignorant but I don't see how I could use the pattern matching solution outside of declaring variables.

Result<T, T> not only quite niche, but also looks like an anti-pattern to me. Plus, "either" is commonly associated with the Either type, so having the proposed Result::either method can be potentially confusing.

4 Likes

I don't really agree that it is an anti pattern, I think it is just a regular pattern. The Ok() variant is the value that is given when execution is ok, and the Err() variant is the value that is given it isn't. That being said I do think you make an excellent point about the either type. People may assume that you're casting your error to the either type which is not good. Maybe something along the lines of Result::indifferent_inner() or just Result::indifferent()? Also as pointed out by quinedot there are examples in the std library which use this pattern such as the slice binary search method.

I feel as though this may be somewhat niche but simply because there isn't any really nice ways of handing a return type like Result<T, T>.

The last conversation I remember about this was basically that Result<T, T> is a code smell, and that thus there's no need to add a method for it, and the (Ok(x) | Err(x)) pattern is fine. (IMHO the binary_search method is meh at best, since the much nicer API is equal_range -> Range<usize>.)

Adding one to ControlFlow in std::ops - Rust -- which doesn't have the differing intent between the variants -- would be fine, though.

4 Likes

The reason it's an anti-pattern is that it only allows one kind of error, and the error type doesn't describe what went wrong. What if you want to add a second way the operation can fail? What if you want to display the error to see what went wrong? Normally you want to implement Display on the error that describes the failure.

3 Likes

Result<T, T> is not elegant, because it doesn't specify what kind of error that is. T is very unlikely to be implementing the Error trait, so it's not a proper error value interoperable with Rust's error handling libraries.

Error types usually have into_inner for giving back the original value, and the proposed solution does not support this existing pattern.

Even ignoring that, Result<T, T> is pretty niche, because the whole pattern is only applicable if the function returning the Result needs to give the item back, doesn't care to report why it had to do it. Then for unwrapping either of them, the caller must not care about the error anyway. I don't think this pattern is so common to need a dedicated method, especially that there's nothing especially wrong with the existing solutions using pattern matching or existing helper methods. It's only a tiny bit of syntax sugar for a rare situation, which is a questionable API design in the first place.

3 Likes

An important point I don't see in the above answers is that Result<T, E> isn't just an enum with either a T value or an E value: The use of Result<T, E> is a meaningful statement that the T type is the successful result of an operation the caller was trying to perform, and the E type is error information about why that operation could not be performed. If you really did want "just a T value or an E value", that's what the Either type is for. So the idea of a Result::either() method is probably confusion about why Result<T, E> and Either<A, B> are different types.

The reason Result<T, T> is a code smell is that it's rare for the successful result the caller wanted and the appropriate error information to have identical types. The one way I could easily imagine myself running into in Result<T, T> in practice is a Result<i32, i32> in some weakly typed legacy code where errors are still just "error codes" i.e. numbers, but that's a sign that weak typing / error codes are a problem, not that Result's API is missing a use case.

Another way to answer the original question is that all this talk of Result<T, T> is an XY problem, because return Err(self); is already a semantic mistake: self is not a value describing the error that just happened (I assume; the original code is vague enough it's unclear). Instead you want something that describes the error, like return Err("Unable to do the thing because of blah"); or return Err(DatabaseConnectionError(...));. Even if you really have no detailed information, return Err(()); is better than putting non-error values here.

5 Likes

I don't fully understand the problem. Why not have fn(self, some_db_pool_thing) -> Self if you don't care if it's Ok or Err?