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

Yea.

I’m not sure exactly what Rust does after main (possibly just process exit 0?). One thing is that this provides a code organization for users to define an “after main” behavior, so they can throw errors to the top level and then ? them there, calling into the terminate they’ve defined locally.

I’m not sure if this is something people writing applications want (I don’t know what the patterns are right now around what to do with errors once you’ve surfaced them to main), but I think we could meet those users’ needs as well as making code examples nicer.

After main is right here iirc: https://github.com/rust-lang/rust/blob/master/src/libstd/rt.rs#L62

Yeah, I thought of this. I was afraid that it would fail because in practice one tends to be returning many possible errors that all have to be coerced to Box<Error> (via a From impl that is induced as part of the ? sugar). But we might be able to make it work at some point.

Hmm, yes, maybe not as clear as I thought at first.

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?