Pre-RFC: Throwing Functions

Right, the RFC proposes this as unconditional operation (you as its author know that already, but linking so that other people can read it if they want).

This comment seems to contain the current position of the lang team, quoting it:

I think the general feeling was that it is important to keep catch { x } and fn main() { x } as equivalent. However, we would like to support "auto-wrapping".

With the risk of repeating myself, note that I disagree with those results, I think that its a very small issue if catch and fn main() are not equivalent, but its a pretty big issue if there is auto-wrapping instead.

There are two reasons I think the former is better -- other than the Ok(()), of course:

  1. It maintains the "lets the reader determine at a glance where an exception may or may not be thrown" property from the original RFC motivation.
  2. It continues to work if you change the function to a custom error types in the future, since it does error-conversion that just returning doesn't.
1 Like

I definitely like the "never explicitly construct" direction, and want a throw to also allow creating new errors (in addition to propagating them with ?).

I'm not sure I like any of the three desugars, though. They're clever, and work great internally to the method, but it seems like none of them would allow the caller to foo().ok_or_else(bar) (respectively: not on Result, not on Try, and not an inferrable context), and thus would prevent using throws for anything returning Option today.

(The impl Try desugar is fun, though, and makes me think some common methods should move to Try, such as unwrap_or. I guess things like map can't without ATC, though...)

(I assume you didn't mean "clever" to apply to the fn foo() -> Result<X, Y> desugar, which is just about the most banal one possible?)

Anyway I thought these three were basically the possible desugarings? Can you think of any others?

Couldn't you get the same result by just using Result::map() instead?

The "not an inferrable context" thing is definitely awkward though. That feels like an unavoidable drawback of Rust's type-based method resolution: you can't both leave the choice of type up to the caller and let them avoid specifying what type they want in some way. (If methods were resolved purely based on name, the method itself would be the "some way", as the compiler could infer that the requested type is Option from it, instead of trying to go in the reverse direction.)

The least-annoying way to make the type specified seems to be UFCS: Option::ok_or_else(foo(), bar) would work.

I feel like the primary complaint here is having to use Ok on the result. In which case I propose people affected by this define a macro return! which corresponds to return Ok(_). It’s as intuitive as any other solution, IMO, without any auto-promotion/coercion issues.

2 Likes

What if special-case only the implicit conversion from () to Ok(()) or Some(()), not from any other type? It allows to leave ?; at the end of function, but not disturbs things more.

1 Like

IMO, @est31 made a pretty good case that even this conversion is too implicit in this post.

3 Likes

Another option could be to simply allow Ok as a shorthand for Ok(()). That is, X for any enum variant X(y) where y: ().


This is how it would look like in the example above:

use std::path::Path;
use std::io::{self, Read};
use std::fs::File;

fn read_file(path: &Path) -> Result<String, io::Error> {
  let mut buffer = String::new();
  let file = File::open(path)?;
  file.read_to_string(&mut buffer)?;
  Ok(buffer)
}

fn main() -> Result<(), io::Error> {
  let content = read_file(&Path::new("test.txt"))?;
  println!("{}", content);
  Ok
}
3 Likes

I don’t see the problem. Ok(()) is easy to write, and if the compiler tells me its missing it often means I haven’t finished writing that function.

return Err(..); does feel a bit cumbersome to use vs throw ..;, but it’s not a big deal IMO. (I’ve been giving my error classes an err method so I can do return SomeError::err(..);.)

7 Likes

On the topic of returning Ok(value) vs simply value, I find the explicitness of Ok(()) easy to read, easy to write and easy to scan for. The return type is always clear and always matches the declaration. I’m finding it hard to say the same with the motivating example above.

I wouldn’t hate this change, but I’d definitely prefer for convention to remain the way it is now. I’m just not sure if saving those few characters is worth the loss in clarity and additional ambiguity.

Many comments in this thread seem to be in favour of allowing fn a() -> Result<T, E> { value }, and there aren’t many comments holding the opinion that things should stay the same. I wonder how much of that is because people haven’t spoken up, vs because we are all in favour of the change.

16 Likes

I absolutely agree.

I’m sympathetic to wanting the empty statement to work here, and it can. If the empty statement is given an inexpressible type which coerces to either () or Result<(), T> as necessary (preferring the former), I think that satisfies most needs.

Hi,

First time contributor here, and a relative newcomer to Rust (I’ve been at it a couple of months now, I think?). I can understand wanting something nicer for functions that return Result<(), T>, like coercing an empty statement,

fn main() -> Result<(), io::Error> {
  let content = read_file(&Path::new("test.txt"))?;
  println!("{}", content);
}

though I understand there are probably difficulties with making that work generally.

However, I think introducing throws and throw would be a mistake, because people coming from other languages have expectations for what they mean, and Rust would be using them very differently. Error returns are not exceptions, they don’t propagate. Not only that, but the error you’re ‘throwing’ isn’t returned directly, it’s wrapped in an enum variant. You’re hiding the return type behind some syntactic sugar, which may be great when you know what you’re reading, but it’s a big speedbump for a minor syntax simplification.

All this is very confusing, and a source of friction. When I started to learn Rust, I started with the Book, one of the most frustrating things was existing terms being used for very different things (though I can’t think of other examples now, I guess that might be a good thing?), or new terms being introduced to explain concepts that already had widely-used names (looking at you, epochs :confused:). Please don’t make that problem worse.

15 Likes

I agree with @annie on this. As a Rust newb, I’ve written Ok() when I meant to write Ok(()) more times than I care to admit, but the compiler reminded me, and the reason always made sense to me.

My read of this Pre-RFC is that it appears we’re proposing introducing significant new syntax to get rid of other syntax we don’t like. Chief among my issues with the proposal is the loss of clarity about what is actually being returned by the function. Specifically, 1) Expectations are not set when reading the function signature (as Result<> does not appear) nor 2) are they reinforced at return time with an implied or coerced wrapping of ().

If a fix for Ok(()) is still desired, @rehax’s proposal of Ok as a synonym for Ok(()) seems to be both the least intrusive, and easiest to teach.

And finally, I just wanted to call out that I am so impressed with this community. Thank you for this discussion, @jnicklas!

2 Likes

Why don't you just write

fn foo() -> Result<()> { catch {
    stuff_that_can_fail()?;
    stuff_that_cannot();
    // no Ok(()) necessary
}}

Just because of the awkward double block and non-standard formatting. It's just replacing one weird thing with another. Maybe

fn foo() -> Result<(), io::Error> catches { ... }
2 Likes

Agreed absolutely. For me the strength of this feature is only secondarily that it gets rid of Ok(()), and primarily that it means everywhere that an error can be thrown is explicitly marked as such with ?.

1 Like

I strongly dislike the idea of using language constructs to modify types and their values. It’s not that this example is so egregious, it’s more a Pandora’s box thing. If we accept this, the rust community will never see the end of such requests for ad-hoc language level sugar. And what does this buy us? The gain in code is often at most a couple of SLOC, so it doesn’t cut any significant amount of boilerplate. It uses magic syntax, so it doesn’t add to clarity as it’s yet another abstraction to account for while you’re trying to accomplish your original, high level goal. I have to conclude that contrary to the statement that Ok(()) is confusing to new users (something they’ll get over in less than 5 minutes, BTW), now they would have to deal with a way of handling errors that looks somewhat like the exceptions they might be used to, but behaves very differently. How’s that for confusing?

Lastly, there’s a bit of a bikeshedding argument. It does not sound like a good idea to me to associate terms (i.e. throw and catch) already linked to a rather poor way of doing error handling (Exceptions) to something that is not only fundamentally better, but also very different in nature.

10 Likes

This is specially true given that we have macros to get some forms of syntactic sugar:

I can currently write:

fun!{ foo(b: bool) -> i32 => throws String {
    if b {
        throw!("Got true".into())
    };
    42
}}

fun!{ bar(b: bool) -> () => throws String {
    foo(b)?;
}}
1 Like

I’ve always thought that this problem could be better solved by the following syntax:

fn main() -> Result<(), io::Error> = Ok({ 
    let content = read_file(&Path::new("test.txt"))?;
    println!("{}", content);
})
3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.