RFC Mentoring Opportunity: Permit `?` in `main()`

So for my tests I want them to mirror their production usage as closely as possible - this means using ?

Because I am forced by the compiler to use the fn() -> () signature, I am then forced to unwrap (not how used in prod) or pattern match explicitly (extremely tedious, can make a mistake easier with assert, and rarely used in prod).

Not to mention I can’t even use ? in example rust code… Which seems to me the biggest way to encourage new users to not unwrap, when it isn’t present in every example for a result returning function…

Lastly, and this is less of an issue before ? came into being, converting code in functions that use ? for testing the implementation is a annoying to rewrite everything with unwrap.

1 Like

Maybe I’m missing something, but my immediate reaction to this (given that we don’t have catch) is to just wrap the part that returns a Result in a separate function.

fn run() -> Result<(), Box<Error>> {
    some_stuff()?
}
fn main() {
    run().unwrap()  // or deal with the Result in some other way
}

This doesn’t seem all that unergonomic to me, and avoids introducing some special case for main(). Or does everyone think this is too much?

Yes, this is the "do nothing", and it should be considered. Keeping the language simpler has significant advantages in 15+ years.

Alternatively:

fn main() {
    (||{
        some_stuff()?
    })().unwrap()
}

But that might be too much magic syntax.

2 Likes

When I first encountered Rust, I was annoyed that main() didn’t take command-line arguments and return an exit code, it seemed gratuitously limited. When I learned how serious Rust was about cross-platform support, it made a lot of sense: for example, Unix gives each process an array of command-line arguments, but Windows gives each process a single big string. Unix lets each process return a u8 exit code, while Windows exit codes are u32. Embedded platforms often don’t have a concept of “arguments” or “exit codes” at all.

It seems dishonest (in a way the Rust stdlib has avoided dishonesty in the past) to give main() some particular signature when we know it may not be sensible or even possible on every platform, in the way that C and C++ have done. On the other hand, because of C, most platforms will have some story for command-line arguments and exit codes, so maybe it’s not a terrible idea.

Perhaps we can have per-platform main wrappers in the stdlib. For example, std::os::unix::main_wrapper():

fn<F> main_wrapper(f: F) -> ()
    where F: FnOnce(args: &[&str], environ: &[&str]) -> Result<u8, Error>

…and also std::os::windows::main_wrapper():

fn<F> main_wrapper(f: F) -> ()
    where F: FnOnce(args: &str) -> Result<u32, Error>

…and maybe even std::os::generic_main_wrapper():

fn<F> generic_main_wrapper(f: F) -> ()
    where F: FnOnce() -> Result<u8, Error>

…that does not promise your exit code will actually go anywhere, but it should be implementable everywhere.

…and then code examples could look like:

use std::os::unix;

fn main() {
    unix::main_wrapper(|_. _| {
        "sasquatch".parse::<u32>();
    })
}

That’s a bit of extra boiler-plate, but hopefully not too difficult to explain or hand-wave in introductory texts… especially if the generic main_wrapper() is in the prelude.

Here’s an alternative alias to Throws<T>:

fn main() -> Fallible<()> {
    try_something()?
}

or even:

fn main() -> Fallible {
    try_something()?
}

where T: ()

I believe that the meaning of Fallible should be clear as well as searchable for novices.

An alternative to catch can be recover.

4 Likes

Yes, I like the name Fallible. I hadn’t thought of the () default. That’s nice, actually. =)

There is nothing dishonest going on here. Having a main() that returns a Terminate value is portable to all platforms. All platforms support unwrap(), and main() is already invoked from a wrapper; it just happens that the wrapper expects main() return () right now.

6 Likes

FWIW, I brought up this general idea in Modeling <strike>exceptions</strike> thread panics with!

https://docs.rs/error-chain/0.7.2/error_chain/macro.quick_main.html makes that very easy, but both the manual and the macro versions require a new user to be aware of the issue before they start implementing main(). That initial cognitive overhead is the main thing we need to address.

I didn’t know this, but I just checked and discovered that main can be called like any other function. It seems to me that you should never do this. But since it is just a function, I’m a little concerned with a proposal that lets its return type be omitted unlike every other function.

2 Likes

We could call it from above like some parameterized main: F where F: FnOnce() -> T, T: Terminate, and both () and Fallible implement Terminate. You’d still have to explicitly write which return type you want.

I don’t think you understood, this is a valid Rust program:

fn main() { }

fn foo() { main() }

If we allow users to return something other than () without providing a return type, as some of these proposals would have us, that could become a type error without the signature of main explicitly changing.

I’m suggesting we still require an explicit signature, just with more possibilities. So fn main() {} would still work as today, and fn main() -> Fallible {} would be used when you want to use the ? operator. In the latter case, foo() has to update as well.

1 Like

Sure. There are a lot of proposals being thrown around, and I’m raising a concern about some of the proposals.

An important point I didn't see anyone mention yet is what should actually happen at runtime when a program "returns an error from main" as opposed to panicking or returning ().

I can think of two options:

  1. Do what panicking in main does today.
  2. Print the error value we got, then exit normally.

I am strongly in favor of not doing #1, because it would put a giant asterisk on the claim that "? and Result are better than .unwrap() and panics" if ? does the same thing as panic in the simplest possible case. Plus, as useful as backtraces are in the general case, a real error value designed by other humans is likely to be far more useful to the person who needs to debug whatever just happened.


As for the details of how this should work, if we go back to how Niko framed the problem...

I see two basic approaches to solving it:

  1. Allow main() to return more kinds of things (in particular, results).
  2. Change the ? means when it is used in main().

As described in Niko's original post, these seem not so much different approaches as extreme ends of a spectrum, and I believe the optimal solution is probably somewhere in the middle.

The "Fallible" suggestion made above, at least the way I interpreted it, is exactly what I'd want. The main() function does change signature, but only in the simplest possible way (there is exactly one new signature, only one type in that signature, no sigils besides "->", and no invisible/magic bits). The ? semantics also change slightly since it has to convert to this "Fallible" type instead, but conceptually it's still doing the same thing. The possibility that "Fallible" is just one concrete type with no parameters or bounds or impl keyword seems like a really nice touch, since this is supposed to help novices.


Of course, all of that assumes it's really feasible for all "sane" error types that one might try to use ? on in main() to be implicitly converted to a single Fallible type that can then be used to print all the information provided by that error. I think it is, because "implements the Error trait" seems like a good definition of "sane error type", and the conversion to Fallible could just be calling description() and cause() repeatedly to make a big String. Maybe the Fallible type is nothing more than a String with a special print method?

@withoutboats I could imagine a scheme where if we all ? in main but it also has the type signature fn() everything could work out.

That is today ? is analgous to:

match e {
    Ok(e) => e,
    Err(e) => return Err(e)
}

We could just define it in the main function as:

match e {
    Ok(e) => e,
    Err(e) => std::rt::main_error(e),
}

where main_error is something like fn<E: Error>() -> ! (or something like that)

In that sense the behavior of ? changes, but the type signature of main remains the same (and faithful) to the actual type.

1 Like

Edit: Sorry if this is a bit rambly; it’s hot and I only just woke up. Brain might not be in completely working order just yet.

I think it would be a mistake to have ? behave differently in main. All that does is trade the possibility that some copy+paste coding will work versus now having to explain that the behaviour of ? is context-dependent, and having the behaviour of ? be context-dependent.

If this is principally based around helping new users and people who are copy+pasting code from examples without understanding what’s going on, and then being confused when it doesn’t work, then I don’t think any of the proposals actually address that.

First of all, let’s say we allow main to have a different signature that permits errors to be returned. Given a user who is blindly copy+pasting code, or trying to write error handling without understanding how error handling works, how do they know they need to change the signature of main? This is almost the same as just using a try_main function and having main call that: it only works if you know to do it.

If we give main special, magical properties (which includes giving ? magical properties inside main), we now have a new problem: the moment the user tries to move code that works in main to some other function, it’ll stop working. At that point, we’re back to square one again. Plus, we now need to explain this magic behaviour, and users need to remember it.

Any solution to this needs to be global and consistent across all functions: it cannot just affect main. It also has to be something that requires no knowledge or action from new users.

Given that, I can only think of one solution to the novice user problem (that doesn’t involve completely changing how error handling works): add a diagnostic that specifically detects “tried to use ? in a function without a Result return type” and tells the user what to do. As in, it flat-out tells the user the return type they should use, as accurately as possible. The user should be able to copy+paste from the error message into their code. This should work everywhere, no matter the function, not just main.

At that point, having main allow for Result<(), _> as a return type is a logical next step, so that users can act on what the diagnostic tells them. I also think it should be Result: even if the compiler internally uses some trait to wire this up, we should probably only ever mention Result to new users, or it might confuse matters (“so I can return ExitStatus from main instead of Result, why can’t I do that for other functions?”).

That said, I think the important part is the diagnostic. Changing the return status of main is a nice little improvement, but it only improves one function, and only helps in the specific case of trying to blindly copy error handling code into main as opposed to any other function the user may be writing.

9 Likes

Changing return type of fn main also allows easier and more automatic mapping to zero/non-zero exit code.

Currently if error is handled manually (e.g. with match), user may do println! (to stdout instead of stderr by the way), but forget to ::std::process::exit(1).

6 Likes

I do think requiring users to annotate main with a new return type is not a bad idea.

2 Likes