Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try! not usable in main() #12130

Closed
jfager opened this issue Feb 9, 2014 · 18 comments
Closed

try! not usable in main() #12130

jfager opened this issue Feb 9, 2014 · 18 comments

Comments

@jfager
Copy link
Contributor

jfager commented Feb 9, 2014

try! isn't directly usable in main, because it returns IoResult.

extern mod std;

use std::io::IoResult;

fn something() -> IoResult<bool> {
    Ok(true)
}

fn main() {
    let a = try!(something());
    println!("{}", a);
}

fails with

<std macros>:2:59: 2:65 error: mismatched types: expected `()` but found `std::result::Result<<generic #15>,std::io::IoError>` (expected () but found enum std::result::Result)
<std macros>:2     ($e:expr) => (match $e { Ok(e) => e, Err(e) => return Err(e) })
                                                                     ^~~~~~
<std macros>:1:1: 3:2 note: in expansion of if_ok!
if_ok_main.rs:10:13: 10:33 note: expansion site
error: aborting due to previous error
@alexcrichton
Copy link
Member

Closing as intended behavior.

The if_ok! macro is supposed to be a convenience, not a one-stop-shop to error handling. The main function returns () instead of an error, so you cannot use the if_ok!() macro in the main function.

Two other options you have are:

  • unwrap() - not that great of an error message
  • match + fail!() - better error message, slightly more verbose.

@jfager
Copy link
Contributor Author

jfager commented Feb 9, 2014

IO directly in main is common in small scripts and utilities, where you're most interested in convenience. unwrap() is probably fine for these use cases, but it's frustrating that if_ok! falls over here given that it's blessed enough to be a globally available macro, it's likely going to be common in example code, and it's not apparent from the name what it's doing under the covers.

This is kind of a nebulous complaint, I know. I'm not asking for if_ok! as currently conceived to start working in main. But I think at the very least it's a ding against if_ok! being promoted widely as an alternative to verbose IO error handling, because limitations like this are going to bite people and just generally be irritating.

@alexcrichton
Copy link
Member

IO directly in main is common in small scripts and utilities, where you're most interested in convenience

If you're willing to write if_ok!, how much different is it from unwrap? You can also always write your own version of if_ok which fails by default.

and it's not apparent from the name what it's doing under the covers

That's the subject of #12037. The macro itself is quite simple, and that is an explicit goal of the macro to stay that way.

But I think at the very least it's a ding against if_ok! being promoted widely as an alternative to verbose IO error handling

I disagree. I think that there is no one solution for error handling and you need to understand the primitives that are available to you and make the right choice based on that. The if_ok! macro is simply not designed to handle errors in main, it defers to utilities such as custom macros, unwrap, or an explicit match.

because limitations like this are going to bite people and just generally be irritating

I/O errors in general are a little irritating. Robustly handling errors is fairly uncommon and the standard library shouldn't be helping you sweep errors under the rug. You should know exactly where all errors can happen and then as a consumer of a library you decide what to do about it. The libraries give you the tools necessary to handle these errors.

@jfager
Copy link
Contributor Author

jfager commented Feb 10, 2014

I'm not trying to help anyone "sweep errors under the rug", I'm saying that if_ok! in its current form has limitations that are annoying for a blessed global macro that's going to be frequently highlighted as the best way to make otherwise verbose IO handling tolerable (see, for instance, the final edit on this post from Reddit today).

If the official position is for people to be casual about writing their own alternate macros, why provide if_ok! at all?

@alexcrichton
Copy link
Member

if_ok! in its current form has limitations that are annoying for a blessed global macro

This is where I think our understanding of this macro diverges. The if_ok! macro is not the error handling strategy, it is a strategy. I have yet to be convinced that there is "one strategy to rule them all", and approaching any solution expecting it to be the "one strategy" I think is the wrong approach.

The if_ok! macro is provided by default for a few reasons. Primarily it is meant to be one of the most common things you reach for when handling errors, so there's no need to define it slightly different in all crates. Another reason is that macro_rules is gated by default, while macro usage is not. We do not want to force all crates to opt-in to macro rules just to get nice error handling, that would defeat the purpose of the feature gate.

The main function is where everything in rust must be handled. You know that the main function is the last possible thing which could be dealing with the error, and that needs to be taken into account with how to deal with it. Continuing to punt the error up the stack is simply not an option, and that's the whole purpose of the if_ok! macros, so it's fundamentally not designed for this use case.

@jfager
Copy link
Contributor Author

jfager commented Feb 10, 2014

It's frustrating that you're putting words in my mouth. I don't want to sweep errors under the rug and I'm not thinking of if_ok! as the "one strategy" for error handling. I'm saying that people are going to reach for it first because it's a blessed macro that's going to show up widely in example code. Wanting the experience of using a prominent feature to be smooth is not the same thing as wanting a magic cure for all possible IO error handling difficulties.

What benefit does the user get from if_ok! not working in main that pays for the cost of having to bang their head against the error?

I've heard two candidate benefits in this thread:

  1. It's simple, and providing it means users won't have to define it themselves. I appreciate that, but as a user I'd prefer the std lib to give me something that sweats the details a little more. The simplicity is primarily of implementation; the simplicity of use hits snags like this that don't seem otherwise beneficial.
  2. main is the last opportunity to clean up and the limitation prevents you from forgetting that. But what does this actually mean? There's no similar restriction against using unwrap, which is going to fail with an error message pointing to the bowels of Result but otherwise have the same practical effect you'd naively expect 'punt the error up the stack' to have from main. If I just want to bail at the point of an error in main, what extra benefit do I get by failing over early return? In both cases the rest of the function doesn't execute and outstanding destructors get run, right?

I don't know what the answer is. Maybe it's revisiting if_ok!. Maybe it's changing how main works so that it fails when you try to return an error out of it. Or maybe it's just living with having to explain how if_ok! is actually implemented on the mailing list and StackOverflow every so often. Anyways, I'm not going to keep harping on it, I'm just frustrated at what feel like mischaracterizations of what I'm saying.

@alexcrichton
Copy link
Member

I'm sorry if I sounded like I was putting words in your mouth, I definitely didn't want to do that. I was just trying to interpret what you're thinking is. I think something that would be helpful to me is a suggestion of a solution to this problem. Do you have on in mind already?

@alexcrichton alexcrichton reopened this Feb 10, 2014
@jfager
Copy link
Contributor Author

jfager commented Feb 10, 2014

Thank you, and apologies if I came across as overly defensive. I do feel bad raising the concern when I don't have a great answer in mind, but a few thoughts are:

  1. Magic main: main handles returns of a certain set of blessed errors by failing rather than as a compilation error. Not great - magic's inherently bad, violates principle of least surprise, etc.
  2. ExitCode trait: Instead of having to return (), main could be allowed to return types that implement a trait for spitting out exit codes. Result could implement it as Ok(_) => 0, Err(_) => 1 or whatever.
  3. Some handwavy 'do' expression: You would use it similarly to how you call a function that uses if_ok! internally now, i.e. the result of the expression is a Result. The main difference is that it doesn't immediately return from its defining function (just like other expressions, if you leave the semicolon off the end it could be the returned value, but it doesn't have to be), and you wouldn't have to wrap every IO invocation inside it with a macro call, the early exit would be implicit inside the expression block. I like the imaginary promise of this one the best but it's obviously not fully thought out and would probably involve the most work to do it right. It might be roughly prototypable as a macro, though, I'll try to put something together this week.

@jakerr
Copy link
Contributor

jakerr commented Feb 11, 2014

Can a procedural macro look at the context in which it's invoked such that if_ok! could look at the return value of the function it is expanded into and could fail! on Err if the return value is not of type Result?

When the return type of expansion context is not Result could it also add a warning span: ''Result error is not handled", if the function is not tagged with some attribute such as '#[ignores_result_errors]'.

I'm not sure if either of the two above are possible. That is looking at context of expansion, and adding warning spans, from within a procedural macro.

@buster
Copy link

buster commented Feb 19, 2014

How about unwrap! ?

extern mod std;

use std::io::IoResult;

fn something() -> IoResult<bool> {
    Ok(true)
}

fn main() {
    let a = unwrap!(something());
    println!("{}", a);
}

@huonw
Copy link
Member

huonw commented Feb 19, 2014

What would a macro do that the method doesn't?

let a = something.unwrap();

@buster
Copy link

buster commented Feb 19, 2014

It's not exactly clear to me what if_ok! is supposed to solve, but that's maybe because the example here doesn't show other cases.

In the code

extern mod std;

use std::io::IoResult;

fn something() -> IoResult<bool> {
    Ok(true)
}

fn main() {
    let a = if_ok!(something());
    println!("{}", a);
}

For the non-error case the line let a = if_ok!(something()); is completely replacable by let a = something().unwrap();, isn't it? It gives me zero advantage in the case something() returns Ok().

When i change the example code to return an error, what is supposed to happen? The if_ok!() will then return some Err()... Am i to handle that case with some match statement anyway? It's back to unwrap() and friends then..

I have not much experience with Rust so i suppose i am missing something, but my thought probably gives some insight from a rust beginner/python convert what may be confusing for other people as well...

@sfackler
Copy link
Member

@buster if_ok! is designed to help bubble IO errors up the chain. For example, you could have some wire protocol and have a method like this:

struct Event {
    id: i32,
    sender: ~str,
}

fn read_event<R: Reader>(r: &mut R) -> IoResult<Event> {
    let id = if_ok!(r.read_be_i32());
    let sender_len = if_ok!(r.read_be_u32());
    let sender = if_ok!(r.read_bytes(sender_len as uint));
    Ok(Event {
        id: id,
        sender: str::from_utf8_owned(sender),
    })
}

It is not designed to be a general purpose "wrap this around anything that gives you an unused result warning" solution.

@jfager
Copy link
Contributor Author

jfager commented Mar 2, 2014

Another reddit thread about the sort of thing I'm saying is going to be very common.

@huonw
Copy link
Member

huonw commented Mar 2, 2014

@jfager I don't think allowing try! in main would address that at all; they seem to want to silently ignore errors and if we did allow try! in main I would fight tooth-and-nail against an "uncaught" IO error being swept under the rug. (I regard return IO errors from main as essentially identical to an uncaught exception.)

There are multiple error handling strategies:

  • manual match; the most basic and most flexible.
  • using a macro around a match (or one of the other methods here) if the same strategy is used repeatedly (see the return_on_err! and unwrap_or_break! macros suggested in that reddit thread)
  • let _ = io_operation(); to ignore errors entirely
  • io_operation().unwrap() to fail on errors
  • io_operation().ok().unwrap_or(~"foo") to substitute a replacement value for an error
  • try! to bubble-up IO errors (same as just not catching an exception) [as loath as I am to suggest renaming it again, maybe it could be called bubble_up or pass_up.)

And anything else defined to work with IoResult or Result.

Magic main: main handles returns of a certain set of blessed errors by failing rather than as a compilation error. Not great - magic's inherently bad, violates principle of least surprise, etc.

The IoResult type and its use is entirely in libraries, the compiler knows nothing about it. I guess we could have a #[legal_main_return_type] annotation to tell the compiler you're allowed to return it from main, but this is ugly.

@jfager
Copy link
Contributor Author

jfager commented Mar 3, 2014

They're trying to learn how to do I/O idiomatically and without a bunch of ugliness. I point it out because it's an instance of someone reaching for try! as the std-blessed I/O macro that shows up in a lot of example code and getting bit by its limitations.

Part of what they're hitting is using try! in a spawn proc, which isn't exactly the same issue as try! in main, but it's close: the reason you can't do it is that the required return type for the proc is (), just like main.

Again, this is not an argument for sweeping anything under the rug. It is an argument that try!'s prominence and limitations cause pointless headaches when there is no 'up the stack' that could deal with a returned IoResult. Wanting try! to fail in these specific contexts is totally reasonable (confirmed by the advice to "use unwrap instead"); the fact that it doesn't feels more like hitting the limit of the interaction of specific implementation details than a solid solution to the problem it's trying to solve.

I'm sorry I mentioned magic main. I wasn't advocating for it, I just threw it out as something that would remove the immediate problem, but of course it would lead to new ones of its own. Please ignore the suggestion. My current hand-wavy hope is that do comes back as something that can eliminate the need for try! altogether.

@jfager
Copy link
Contributor Author

jfager commented Mar 4, 2014

Superseding with #12676, as I don't actually think the problem is just that try! doesn't work in main.

@jfager jfager closed this as completed Mar 4, 2014
@kornelski
Copy link
Contributor

This is fixed in #43301

bors added a commit to rust-lang-ci/rust that referenced this issue Jul 25, 2022
Turn let-else statements into let and match

Fixes rust-lang#11906.
flip1995 pushed a commit to flip1995/rust that referenced this issue Jan 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants