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

Agreed.

Probably Result<(), T> where T: Error but yes.

I agree it's orthogonal but it seems important to reaching the final goal of pretty examples on the front page. =) Without it though we can at least get examples that show off best practices, I guess.

2 Likes

I’m thinking that Fallible might or might not be a good idea (or a good name) but it is not that important. For most examples, after all, there may already be an existing Result alias that one could use. e.g., if the error is an io error, one could use the existing io::Result<T> alias:

use std::io;

fn main() -> io::Result<()> {
    let mut f = File::open("foo")?;
    f.write_all("Hello, world!")?;
}

Now one might argue that io::Result should default its argument to (), but that seems like a minor thing.

5 Likes

An alternative to Throws<T> was needed. The nice thing about Fallible, I think, is its two literal meanings:

  • Liable to err.
  • Liable to be erroneous (e.g. fallible information).

The first definition matches the imperative case where T=(), i.e. a fallible function. The second definition fits a type theoretic perspective such that Fallible<T> is a T that you need to check for errors analogous to the Option<T>.

I'm thinking that Fallible might or might not be a good idea (or a good name) but it is not that important.

If this is mostly about newbies, the documentation and simple code examples, then the naming seems quite a bit important.

For most examples, after all, there may already be an existing Result alias that one could use. e.g., if the error is an io error, one could use the existing io::Result alias:

Especially because this pattern will be almost everywhere - an alias which name contains Result - it would be nice if simple code examples follow it as much as possible and don't use a completely different name.

My idea would be to add this to the prelude:

pub fn run<E: Error>() -> Result<(), E> {
    Ok(main())
}

The runtime would call this function instead of main.

  • If main() is used, everything works as expected.
  • if run() -> Result<(), E> is defined it overrides the default one.

The main “disadvantage” would be to teach defining the run() function instead of main(). This would also break existing code that defines a run() function in main.rs

edited to use ToExitStatus:

pub fn run() -> Result<(), impl ToExitStatus> {
    Result::<(), std::io::Error>::Ok(main())
}

Right now, to write Unix CLI utilities in Rust, you wind up doing something like this:

fn inner_main() -> Result<(), HLError> {
    let args = parse_cmdline()?;
    // all the real work here
}

fn main() {
    process::exit(match inner_main() {
        Ok(_) => 0,
        Err(ref e) => {
            writeln!(io::stderr(), "{}", e).unwrap();
            1
        }
    });
}

So I like the fn main () -> something_Result_ish proposal, because it basically paves this cowpath.

However. It is very important for this use case that returning an Err from main does not trigger a panic. It normally would not represent a bug, and in some cases, it needs to produce no output other than the exit code –

$ grep -q root /etc/passwd ; echo $?
0
$ grep -q notthere /etc/passwd ; echo $?
1
$ grep -q notthere /etc/shadow ; echo $?
grep: /etc/shadow: permission denied
2
5 Likes

I would agree to sebk’s suggestion, except that run is a [C-T] polymorphic function (E isn’t even defined in this case). This highlights one of the major differences between Rust’s strict-type error handling and C++'s exceptions (conventions but no actual rules on the return type).

zackw: If you want to reimplement a Unix utility with exact handling of errors, I think you need to do something like this anyway.

IMO this is more about lazily written utilities and example code doing something sensible without any explicit error handling (other than the ?).

dhardy: I don’t see why we couldn’t find a sensible default behavior that is compatible with doing the Right Thing for CLI utilities. Thinking out loud, there are two cases that are common enough that I think libstd should support them:

  • Successful unless an I/O error occurred. Corresponds directly to Result<()>; the runtime should map Ok to exit code 0, and Err(e) to exit code 1 + print the Display of e to stderr.
  • grep-like: three-way distinction (yes, no, I/O error). Result<bool> can represent this; Ok(true) maps to exit 0, Ok(false) to exit 1, and Err(e) to exit 2 + print the Display of e to stderr.

Anything more complicated than that probably does need to be handled by the application, but ideally “handled by the application” would mean “the application implements a trait for the type it’s going to return from main, defining both the mapping to exit status and what, if anything, should be written to stderr for each case.” Hypothetically

trait ToExitStatus {
    fn exit_status(&self) -> u32;
    fn report_failure(&self, stream: Write);
}

// libstd provides:
impl ToExitStatus for Result<(), E> where E: Display {
    fn exit_status(&self) -> u32 { match self { Ok(_) => 0, Err(_) => 1 } }
    fn report_failure(&self, stream: Write) {
        if let Err(ref e) = self {
            writeln!(stream, "{}", e).unwrap();
        }
    }
}

impl ToExitStatus for Result<bool, E> where E: Display {
    fn exit_status(&self) -> u32 {
        match self { Ok(true) => 0, Ok(false) => 1, Err(_) => 2 }
    }
    fn report_failure(&self, stream: Write) {
        if let Err(ref e) = self {
            writeln!(stream, "{}", e).unwrap();
        }
    }
}

// for compatibility only;
// documentation warns that ? will not work in `main` if it returns ()
impl ToExitStatus for () {
    fn exit_status(&self) -> u32 { 0 }
    fn report_failure(&self, stream: Write) { }
}
2 Likes

How will the exit status work for different -architectures- (replaced with platform)? Do I have to implement it for every -architecture- (platform) ?

@sebk This is not a CPU-dependent thing as far as I am aware.

Are you imagining contexts that do not provide an equivalent of the POSIX _exit / Windows ExitProcess primitives, both of which take an integer exit status? (The only such case that comes to mind is Plan 9 with its string exit status.) I would suggest that such runtimes would supply their own, deliberately incompatible specification of the ToExitStatus trait, in which exit_status returns some other type or doesn’t exist at all, and suitable #[cfg] tags. Programs that use the stock impls in libstd do not need to change. Programs with their own impl will need to reimplement for the altered signature if they care about the exotic runtimes in question.

1 Like

@zackw architecture wasn’t the correct term there. Maybe platform is better.

I mean the mapping of return code to “semantic meaning” (how it is handled) is not uniform between Linux, Windows, etc.

Would every Err value create the same exit status, or would there be different values for, say OutOfMemory, File::read failed and DNS lookup failure?

If they all share the same numerical value, (assuming Ok => 0, Err => 1 for platform X), do we even need ToExitStatus?

@sebk Unixy systems mostly map all “failure” exits to exit code 1, or sometimes 2 (as with grep, where code 1 means “no matches found”). There have been a few attempts to standardize a richer set of meanings (e.g. BSD sysexits.h) but mostly no one bothers with them. I don’t have enough experience with other platforms to talk about them.

If there is a genuinely widely used convention for mapping system errors to exit statuses on Windows (for instance), then Rust ought to conform - but that would be a separate thing from ToExitStatus. The point of ToExitStatus is that the application can define what it means by a Result returned from main, with a couple of stock options. The mapping from io::Error values to platform-specific process exit statuses is properly io::Error's business.

1 Like

I do get the idea, but I am not sure ToExitStatus is the optimal solution.

type OsResult = Result<(), OsError>;
fn run<R: Into<OsResult>() -> R;

The above would work too, except one would implement Into<OsError> for the different Error types.

I certainly don’t mean to be saying that I think ToExitStatus is optimal, it was just the first thing I thought of.

Maybe we should back up a bit. As I see it, the design goals here are, in decreasing order of importance:

  1. The ? operator should be usable in main.
  2. Existing code with fn main() -> () should keep working, but it’s ok (IMHO) to have to change the signature of main if you want to use ? inside, and maybe the empty return value can be deprecated.
  3. When ? is used in main, its effects should be consistent with platform conventions for process termination. These often include the ability to pass an “exit status” up to some outer environment, conventions for what that status means, and an expectation that a diagnostic message will be written to stderr when a program fails due to a system error.
  4. Since we are messing with main anyway, let’s make it easier to program to those platform conventions; right now it’s a little clumsy.
  5. We should avoid making life more complicated for people who don’t care; on the other hand, if the Easiest Thing is also the Right Thing according to the platform convention, that is better all around. (I do not like how, at present, “all errors are panics” is the very easiest thing, and “the program returns exit code 0 whether or not an error occurred” is the second easiest thing.)

Can we agree on these, before we go back to talking about how to implement?

1 Like

I do agree on 1, 2, 3 and 5. How № 4 can be solved is unclear to me. Also in my opinion a simple main() signature would be great.

The following might cause confusion, as the return type is not clear.

fn main() -> impl Terminate {
    Ok(2+2)
}

Another idea, that this example shows is the ability to return anything that implements Display.

I believe it is important to ensure that user isn’t required to use some new signature for the main. Other than that i believe alternative signature e.g. Result<(), Err> should be provided by user if he wants to return some error.

This error most likely should be able to provide some short error description which would go to stderr and optional exit code (if there is no exit code then we could just return any non-zero) which is used if possible for target. Preferably that any kind of error would be acceptable without much of user intervention to wrap some API errors. I.e. just use accept object with std::error:Error? Maybe introduction of optional ExitCode trait for Error (is it possible to have optional trait?) if user wish to customize return code, but it must be up to compiler to decide how this code is propagated to system.

1 Like

How about some nifty macro like that:

#[result_to_exit]
fn main() {
    let mut f = File::open("foo")?;
    f.write_all("Hello, world!")?;
}

That would take the main body and make it main_inner and handle the return value.

This way it could be implemented as a standalone crate, we could see what community thinks, and if well received, it could be added to std one day, maybe.

I do like this approach. Aside from the syntax, and not yet supporting a derive macro, that is basically what quick_main from error_chain does.

I think the main point of the RFC is - or should be - that simple examples don’t use unwrap, have per default a sensible error handling with the aid of ? and that beginners can use the signature of main as an example how they can write their first own functions.

Therefore I would strongly prefer if the result value of main would be named after Result, that beginners see this type/name quite early and get a first idea about it.

I would love if beginners would just see something like:

fn main() -> Result {
    ...
}

If they later see things like Result<u32> or Result<u32, SomeError>, they might already think: oh, this result returns a u32, and this other one has some special kind of error.

4 Likes