Allow convert T into Ok(T)

Suddenly, I have noticed uncomfortable pattern in my code:

fn function() -> Result<T, E> {
    // Do some stuff

    Ok(
        // Some chained call
        variable
            .do_stuff()
            .do_try_stuff()?
            .do_final_stuff()
    )
}

So I was thought about T.into() chained call for this case:

fn function() -> Result<T, E> {
    // Do some stuff

    // Some chained call
    variable
        .do_stuff()
        .do_try_stuff()?
        .do_final_stuff()
        .into()
}

I think that this helps to make code more clear. And It must not break any existed code, because into Result must be explicitly invoke by programmer (due to the orphan rule, If I understand all correctly):

Into::<Result<T, E>>::into(value)

I will glad to see any advices in such kind of situations and accept any critic. Have a nice day :D

Update 2023 / 02 / 05

After some discuss I decided to extend this post by several ideas. As for today we have these possible solutions...

Implement T -> Ok(T)

impl<T, E> From<T> for Result<T, E> {
   fn from(t: T) -> Result<T, E> {
       Result::Ok(t)
   }
}

Pros:

Cons:

  • No option for E -> Err(E)

Implement T: !Error -> Ok(T), E: Error -> Err(E)

impl<T: !Error, E> From<T> for Result<T, E> {
    fn from(t: T) -> Result<T, E> {
        Result::Ok(t)
    }
}

impl<E: Error, T> From<E> for Result<T, E> {
    fn from(e: E) -> Result<T, E> {
        Result::Err(e)
    }
}

Pros:

  • Covered case when programmer wants to return E.into()

Cons:

  • Cannot be implemented right now due to negative traits problems: Github rust-lang issues
  • Probably will not implemented for several years?

Implement T -> Ok(T), E -> Err(E); Result<T, E>; T != E

I was thought and now I think that this is the best choice

impl<T, E> From<T> for Result<T, E>
    where T: !E
{
    fn from(t: T) -> Result<T, E> {
        Result::Ok(t)
    }
}

impl<T, E> From<E> for Result<T, E>
    where T: !E
{
    fn from(e: E) -> Result<T, E> {
        Result::Err(e)
    }
}

Pros:

  1. Independed from any traits (depends only on T != E)

  2. Result<T, T> must be covered explicitly (and that is good)

  3. Cover all possible cases (since 1 and 2 - opposite cases)

  4. Probably have less problems that previous solution

    I think so because we can represent T != E as T = !E so we can deal with ONE type and ONE negative impl (if I'm not wrong)

    impl<T> From<T> for Result<T, !T> {
        fn from(t: T) -> Result<T, !T> {
            Result::Ok(t)
        }
    }
    
    impl<T> From<!T> for Result<T, !T> {
        fn from(e: !T) -> Result<T, !T> {
            Result::Err(e)
        }
    }
    

Cons:

  • Requires some support from community to help rust core developers notice this idea :D
4 Likes

impl<T, E> From<T> for Result<T, E> will conflict with impl<T> From<T> for T however ?

I'm counting on type annotations. In my cases and all cases where type annotations are used - compiler must choose From<T> for Result<T, E> over From<T> for T in any place when type annotation is Result<T, E>

I hope I explained why there is no conflict

Sure, but currently the compiler won't accept From<T> for Result<T, E> because, given a Result<T, E>, it won't know whether Result::<T, E>::from should call that impl or the blanket From<T> for T one.

Perhaps specialisation can overcome the conflict.

The compiler does accept it. [play.rust-lang.org]

2 Likes

The only inconsistence I see on this conversion is, what if original value is supposed to be the Err variant? Otherwise it seems handy!

I was thought about negative traits which are not supported for now:

impl<T: !Error, E> From<T> for Result<T, E> {
    fn from(t: T) -> Result<T, E> {
        Result::Ok(t)
    }
}

impl<E: Error, T> From<E> for Result<T, E> {
    fn from(e: E) -> Result<T, E> {
        Result::Err(e)
    }
}

But this will not work with any type (e.g. Result<T, u32 /* error code */>) because bounds will overlap

1 Like

The better solution to this would be try blocks.

fn function() -> Result<T, E> {
    // Do some stuff

    try {
        // Some chained call
        variable
            .do_stuff()
            .do_try_stuff()?
            .do_final_stuff()
    }
}
4 Likes

Or if you find this pattern prevalent in a particular crate, it's just 6 lines of code:

trait IntoOk: Sized {
    fn into_ok<E>(self) -> Result<Self, E> {
        Ok(self)
    }
}
impl<T> IntoOk for T {}

Then you can chain .into_ok() on anything; or even .into_ok::<ErrorType>() if the error type cannot be inferred (unlike the case with Into where it is the trait that is generic not the trait method).

3 Likes

*Please notice, that I've update post

Your idea is also good, but, if you ask me, I don't want to produce new traits (nor in custom code, nor in core). I will use your solution (it's really cool), but I hope that our discus will continue until implementation

Given that OP wanted to get rid of the indentation in favor of chaining and no indentation, the difference between an explicitly try{} -wrapping version and an explicitly Ok()-wrapping version is pretty negligible I'd say.

Arguably slightly worse even since it replaces something maximally explicit (calling a value constructor) with what could be seen as an abstraction of it.

4 Likes

Actually I would just write this:

// Some chained call
let r = variable
    .do_stuff()
    .do_try_stuff()?
    .do_final_stuff();
Ok(r)
6 Likes

He comment just what I wanted to answer, but decided to quite. I can't add anything other than this: less indentation is better.

Also, I hope, that this implementation will allow some handly implicitly casts (same as T.into() for Option<T>)

While I agree in general about having less indentation, I find the suggested solution to be worse than the issue it is trying to solve.

My preferred approach would be try blocks because in particular they could also reduce nesting by allowing us to write functions like so:

    fn foo() 
    try {
    // .. code goes here
    }

In other words, there shouldn't be an additional nesting required since the function's body is itself a block.

2 Likes

If you want use a post-fix Ok right now, you could use the combinators provided by the crate tap, i.e. in this case, ….pipe(Ok) should work.

2 Likes

I like Rust simplicity and I don't want to have neither try fn function(), nor fn function() try. Macros allow to have such syntax right now so if you already tried and like it - I can't say something more except following: we should solve design problems use as less new syntax as possible.

For now, I sure that such syntax fn function() try {} or fn function() = try {} (saw somewhere here) is not good.

I think that chain syntax is very easy to read and understand. So I propose to build some features on top of existed implementation (at least T -> Ok(t) see update and \ or read comments above)

If you don't want to use new language features than your best bet imo is to extract a variable as suggested a few comments above.

Having x.into() instead of Ok(x) is objectively worse - it is simultaneously more verbose and less useful for the reader as it conveys less information.

There are ways to improve the error handling story of Rust but this is not one of them.

3 Likes

And this can be done today using fehler::throws as a proc-macro implementation of (Result specific) try fn. I personally find this increases readability enough to offset the proc-macro cost and use it in all my crates that already depend on the proc-macro ecosystem.

3 Likes

x.into() is not more verbose than Ok(x) unless there is no indentation. But, yes, it's objectively not clear.

1 Like

fehler::throws looks interesting! Any reason why the standard built-ins don't include something like that? @yigal100's suggestion, fn f() try ... also looks good, but how will one throw Err with that syntax?

1 Like