Implicit void result Ok(())

I could not find this discussed before, so apologies if this was raised before. I find myself writing quite a lot of Ok(()) at the end of functions which return void Result. I was wondering whether the language couldn’t just implicitly insert it in a place where a -> Result<(), Error> function doesn’t have a return value, like:

fun foo() -> Result<(), Error> {
  bar()?;
}

Any Error type would be supported. Interested to hear others’ opinions, but will understand if this doesn’t fit in with the language design strategy of the language.

Edit: Note that the last line of the function might not necessarily evaluate to a Result<...>:

fun foo() -> Result<(), Error> {
  bar()?;
  baz();
}
1 Like

This proposal is relevant: Pre-RFC: Catching Functions

Ah, so this does seem to be widely discussed. I can see problems with implicitly wrapping anything else besides (), as it’s not obvious what happens in this case:

fun baz() -> Result<T, Error> {
  ...
  x // x actually is already a Result<T, Error>
}

so I think this small proposal might be more palatable. Either way thanks for pointing me in the right way, very interesting discussion!

Seems to me that we really just want a

mod std::ops;
trait EmptyReturn {
  const RETURN: Self;
}

impl EmptyReturn for () {
  const RETURN: Self = ();
}

impl<T, E> EmptyReturn for Result<T, E>
  where T: EmptyReturn {
  const RETURN: Self = Ok(<T as EmptyReturn>::RETURN):
}

with the obvious desugaring: if a block-returning-R’ does not end in an expression, insert <R as EmptyReturn>::RETURN as that final expression. See also for return with no argument.

This should be a constant for what I hope are obvious, spooky-action reasons (as-is this feature is already terrifically spooky).

3 Likes

More previous conversations in this arena:

Historically this has been a huge timesink of a topic, so I would encourage people to think carefully before trying to re-open it.

11 Likes

By the way, in cases where you have several (one or more) Result-returning function calls in a row, you can just omit the final unwrapping and return the return value of the last call directly:

fn foo() -> Result<(), MyError> {
    bar()?;
    baz()?;
    qux()
}

This obviates the need for an explicit Ok(()) at the end. (You could also .map(drop) at the end, if the last call doesn’t return Result<(), MyError>, although not sure if it’s a readability improvement.)

1 Like

That only works if the final one doesn’t need error-conversion, so I prefer doing something that allows all the calls to be consistent in how they look, such as doing

fn foo() -> Result<(), MyError> {
    bar()?;
    baz()?;
    qux()?;
    Ok(())
}
3 Likes

I know, although my note about .map(drop) also applies to .map_err(…). It’s not that I prefer this style; I merely wanted to point out an alternative because some people apparently find Ok(()) annoying. I don’t share that sentiment and I prefer the more uniform style too – my goal was primarily to show how implicit Ok-wrapping and/or special-casing Result<(), _> (both of which I’m strongly against) isn’t really necessary based on OP’s example.

3 Likes

This is not a very good argument, if I may say so. It goes along the lines of “we don’t need X, to replace Y, because there’s a worse way of doing things than Y, sometimes, which is Z”.

This suggestion is about code that already has Ok(()), as the “best” current way of doing things, and how it could be improved. Wrapping the previous line in Ok() or .map(drop) is not a real alternative, imho (that line might not even, and often doesn’t, evaluate to Result<...> - my example is just the simplest piece of code I could come up with to illustrate the scenario). Let me add the example to the post so it doesn’t tempt people to go down this route.

1 Like

It’s not a “worse way” – some like it, some dislike it, but it’s a minor point. I have an entire line of arguments why Ok-wrapping isn’t a good idea, which I’m definitely not going to reiterate here because it has been discussed in the past several times.

5 Likes

I would like for there to be some way of avoiding Ok(val?) or val.map_err(Into::into) for return expressions, but I’m not sure what that could look like. Not too much of a fan of implicit Ok(()) return, as I think it might end up being too magical.

1 Like

There is currently the ::candy::fallible! macro to get a dummy but useful auto-wrapping (the only case where it is not great is when wanting to return an explicit Ok(..), since it requires to use an early return to escape the wrapping). But I find it very convenient in general, and allows to split the Ok and Err case return types over two lines :slight_smile:

Consider also a function like this:

// Assume Ok() wrapping exists.
pub fn try_write(file_name: &str) -> std::io::Result<()> {
    std::fs::File::open(file_name)
        .map(|f| /* write some data */ ());
}

Can you spot the mistake? What happens to file open errors?

2 Likes

At the minimum, you’ll get an unused-must-use warning.

3 Likes

Sounds good whereever it’s obvious that instead of () an Ok(()) was meant. But what if the function returns Result<(), ()>?

Many proposals require using try fn for explicitly enabling Ok-wrapping:

// will result in a compilation error
fn foo() -> io::Result<()> {
    do_stuff()?;
}
// will work
try fn foo() -> io::Result<()> {
    do_stuff()?;
}

So you will not be able to use Ok-wrapping in || { .. } style closures (though it will be possible to add try || { .. } later).

1 Like

I’ll point you to https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777,

In short, given that our attempts to find more universal coercions have failed, it seems clear that if we are going to do some form of ok-wrapping, it has to have syntactic opt-in.

There was a bunch of exploration of invisible ways to do this, and none were found with which people were happy enough with which to move forward, so I don’t think you need to worry about the scenario you raised.