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.
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.
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.
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
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 –
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).
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) { }
}
@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.
@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.
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:
The ? operator should be usable in main.
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.
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.
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.
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?
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.
#[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 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.