Rust: Stabilize `main` with non-() return types

Created on 23 Feb 2018  路  28Comments  路  Source: rust-lang/rust

This is a sub-issue of the larger tracking issue devoted to the ? in main RFC. It is specifically proposing that we stabilize the behavior that permits main to have a return type other than (). This return type must implement the Termination trait. The details of the trait itself (including its name and so forth) are not stabilized.

This commits us to the following (each link is to a test):

The changes from the RFC are as follows:

  • we modified the Result impl to use Debug

The following unresolved questions will be resolved:

But the following is not stabilized and free to change:

  • the name/location of the Termination trait and its methods

    • likely changing to std::process::Termination

  • the usage of -> Result<...> in unit tests (not yet landed)
  • the ErrCode newtype wrapper for returning a custom error code

--

UPDATE 1: Restricted the set of termination impls as proposed by @scottmcm in https://github.com/rust-lang/rust/pull/48497.

C-enhancement E-mentor T-lang disposition-merge finished-final-comment-period

Most helpful comment

:+1: to stabilizing the basics here.

:-1: to some of the allowed return types.

I would propose only stabilizing the following new return types:

  • !
  • Result<(), E: Debug>
  • Result<!, E: Debug>

Thoughts on the others:

  • Arbitrary nesting is weird. Why would one have a Result<Result<!, io::Error>, fmt::Error>?
  • Nesting also makes return values weird, like with Result<i32, _> you run into collisions with Ok(1) and Err(_).
  • I still prefer some domain-specific type (strawman ExitCode) to i32. I could accept just fn main() -> i32, but it feels like primitive obsession (especially if nesting happens, where it's meaning is far less clear). There were some interesting discussions in the RFC about platform differences here too, which feel like they could be a good fit with the "vision for portability" stuff. And this case in particular isn't needed for the ?-in-main goals, so can easily wait. As a bonus, I think having a type here that's not a primitive would let us leave the impl but have have the type unstable as the details are figured out.

I'll happily make a PR restricting things as above if that'd be helpful.

All 28 comments

@rfcbot fcp merge

I propose that we stabilize as described above.

Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged teams:

  • [x] @aturon
  • [x] @cramertj
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

Concerns:

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

:+1: to stabilizing the basics here.

:-1: to some of the allowed return types.

I would propose only stabilizing the following new return types:

  • !
  • Result<(), E: Debug>
  • Result<!, E: Debug>

Thoughts on the others:

  • Arbitrary nesting is weird. Why would one have a Result<Result<!, io::Error>, fmt::Error>?
  • Nesting also makes return values weird, like with Result<i32, _> you run into collisions with Ok(1) and Err(_).
  • I still prefer some domain-specific type (strawman ExitCode) to i32. I could accept just fn main() -> i32, but it feels like primitive obsession (especially if nesting happens, where it's meaning is far less clear). There were some interesting discussions in the RFC about platform differences here too, which feel like they could be a good fit with the "vision for portability" stuff. And this case in particular isn't needed for the ?-in-main goals, so can easily wait. As a bonus, I think having a type here that's not a primitive would let us leave the impl but have have the type unstable as the details are figured out.

I'll happily make a PR restricting things as above if that'd be helpful.

@rfcbot concern delay stabilization of nesting and i32 exit code

I feel basically the same way that @scottmcm does-- nesting (e.g. Result<i32, Error>) seems confusing and hard to reason about, and i32 seems like it should be ExitCode, perhaps with a from_raw method. Can we delay stabilizing these particular impls pending further discussion? I'd love to see ! and Result<!/(), E: Debug> stabilize.

Edit: also bool?

Yeah it is the opposite, wasm is 0 and 1 and all other platforms use libc::*. I have done that, because libc::EXIT_SUCCESS and libc::EXIT_FAILURE were not available at that time for wasm in libc.

I noticed that there's impl Termination for bool right now too (it's not mentioned in the OP), which I don't think should be part of the minimal set that we stabilize in the first pass.

The RFC says E: Display instead of E: Debug, and tbh, Display would have been my first choice as well. But I'm sure this has been discussed.

Should the RFC be amended to reflect this change, and to explain why Debug is the right choice?

@ExpHP yes, you are correct. Something approaching a summary of thinking around Display vs Debug starts around here: https://github.com/rust-lang/rust/issues/43301#issuecomment-362020946.

I'm surprised by the concern about nesting - how would someone come to nest results in their main return anyway? It seems like the natural way to implement termination, which has the consequence of enabling this kind of thing, but that it happening in practice would be a non-issue.

Should RFC be amended to reflect this change, and to explain why Debug is the right choice?

we do not usually amend RFCs to cover changes before stabilization. they are design documents for implementation, they dont play any sort of 'constitutional' role.

@withoutboats, that is interesting.

If I'm understanding you correctly, by allowing Result<T, E>, T itself may be a Result<U, F>, but that we should not invent extra machinery to disallow this? That does make a lot of sense...

Personally, I was only considering what to allow from the perspective of being conservative; but when being conservative mandates extra logic to specifically disallow certain types, I can see the problem. (Such a limitation could also feel quite arbitrary in blocking some scenario we might not be imagining at the moment...)

Thanks, as usual, for the enlightening perspective. :)

Fixed the wasm reference.

I also thought we might want to limit the impls. Note that we have no mechanism for "feature-gating" impls, so the only way to limit things once we stabilize the basic concept is to remove the stuff we don't want.

I think that supporting only the basics (!, (), and Result<(), E>) seems good to start -- I anticipate that anyone who wants to use this feature for more complex things will define their own new-type and implement Terminate themselves. That is certainly what I plan to do in my own code, since I think it's a nice way to separate out the error handling from the rest.

If we wanted to go crazy (I don't), I could imagine adding a new trait like SuccessfulTerminate and then implementing Terminate for T and Result<T, E> where T: SuccessfulTerminate. This avoids nesting but keeps flexibility. (We would implement SuccessfulTerminate for () and ExitCode or something.) But it feels like overkill, and it can be readily modeled with your own newtype anyhow.

@scottmcm

I noticed that there's impl Termination for bool right now too (it's not mentioned in the OP), which I don't think should be part of the minimal set that we stabilize in the first pass.

good catch, I missed that

@scottmcm if you prep the PR, I will update the description

I think the alternative that @cramertj and @scottmcm would prefer is that we replace the Result impl with two impls:

impl<E: Debug> Termination for Result<(), E>
impl<E: Debug> Termination for Result<!, E>

Then its not all types that implement Termination, just () and !.

I wouldn't really mind doing this, but I think philosophically I'd tend to prefer an open system that allows for some confusing cases than a closed system. Having the open impl could allow for use cases involving third party types we haven't thought of. As an example, I could see some framework with a special type that implements Termination, and its idiomatic within that framework is to return from main with Result<FrameworkTerminationType, Error>.

Its sort of the nature of a type class based polymorphism system that we enable extensions we haven't anticipated, including possibly bad ones.

I'd also draw a distinction between designs that make things that we don't like possible and designs that lead people into an outcome we don't like. Does anyone see a way that the current set of impls encourages users to do a nested Result? I don't.

I think all of the impls that exist - including i32 and bool - are fine and good and should exist, but I also don't see the ones that people want removed actually being used very often, so I'd be fine removing them to make progress on the impls that people very clearly want.

@withoutboats ah, I somehow misread and thought that you had registered the concern (but I see now it was @cramertj).

I think I feel the same as you -- I found the nesting a bit odd but probably better on balance than The Right Thing of having more traits, and not necessarily bad.

Also, the design I mentioned is a bit busted, because any design that allows the nested type to specify the exit code means it can specify a non-zero error code, which is traditionally an error result (but not necessarily, there is no fixed interpretation; I mean consider a command intended to be used within the bash if -- is it an error if the condition returns 1).

Anyway, I also just kind of don't care that much. I'm happy to start with a simple set of impls. I'm also happy to start with a more expansive set.

Anyway, I also just kind of don't care that much. I'm happy to start with a simple set of impls. I'm also happy to start with a more expansive set.

Agreed. If @cramertj and @scottmcm still feel strongly after our counterarguments, I wouldn't register a concern in the opposite direction. :-)

Ah, I understand the desire for nesting now if the goal was to reduce the number of traits involved. It seems unlikely that someone would write Result<Result<(), Error>, Error>, so I'm not really worried so much about that case. IMO -> Result<i32, Error> and -> Result<bool, Error> seem more likely to occur in practice, and their behavior is even less obvious, but I'd still be willing to file that under "things that are crazy but that people won't ever actually encounter unless they're intentionally messing with the system".

I still feel fairly strongly that -> i32 and -> bool are in need of more typesafe wrappers.

PR up at https://github.com/rust-lang/rust/pull/48497, with impls as boats guessed.

I added an unstable please-don't-rely-on-this-at-all std::process::ExitCode with an ugly feature name so I could keep the return-a-code-directly impl in a way that doesn't stabilize it; hopefully that's ok.

I'm definitely not saying we'll never re-broaden these impls, but I like stabilizing just the obviously valuable cases now and seeing what comes up, like https://github.com/rust-lang/rfcs/pull/1937#issuecomment-311183670

@cramertj care to resolve your concern?

@rfcbot resolved delay stabilization of nesting and i32 exit code

@nikomatsakis Thanks! Just saw the r+ on @scottmcm's PR.

:bell: This is now entering its final comment period, as per the review above. :bell:

The final comment period is now complete.

I added https://github.com/rust-lang/rust/issues/48854 to propose stabilizing unit tests that return results.

All right, let's do this! Here are some mentoring instructions for preparing a stabilization PR.

The general instructions for stabilization can be found on forge. We are stabilizing the termination_trait (but not termination_trait_lib) feature.

Technically, that stabilizes more than is described here, because it will also stabilize using non-() return types in unit tests. But I've already opened #48854 to vote on that, and I expect it to pass, so we can prep the PR doing both and then just wait to merge.

UPDATE: OK, looks like we may want to split the feature gates after all. See here for further mentoring instructions.

cc https://github.com/rust-lang/rust/issues/48890 for an ICE in this feature

OK, it seems like I was wrong about #48854, perhaps we will wait to stabilize that feature. That means that the stabilization PR for this feature is going to need to do two things:

  • Introduce a new feature gate, covering only the use of this feature in unit tests. (Good first PR)

    • Good first PR!

    • Let's call it #[termination_trait_test] or something.

  • Stabilize the remaining #[termination_trait] use cases.

To do the first part (separation) will involve the following steps:

  • Create a new feature gate, termination_trait_test

    • The process is described in the "Stability in Code" section of this forge page

    • But you might also just run rg '\btermination_trait\b and copy what's there already

  • Modify the code in libsyntax that is gated on termination_trait to be gated on termination_trait_test:

https://github.com/rust-lang/rust/blob/master/src/libsyntax/test.rs#L334

https://github.com/rust-lang/rust/blob/master/src/libsyntax/test.rs#L361

https://github.com/rust-lang/rust/blob/master/src/libsyntax/test.rs#L390

https://github.com/rust-lang/rust/blob/master/src/libsyntax/test.rs#L418

There may be a few other places, not sure.

@tmandry opened up #49162 =)

This stabilization happened; closing.

Was this page helpful?
0 / 5 - 0 ratings