If you've ever had any interest in writing an RFC, I've got a proposal for you. =) The recent release of tokio -- exciting! -- has reminded me about a nagging issue that I think we should try to fix. It's not a big thing, but I think it's an important thing, and it fits squarely into the roadmap's goals around "quality of life" improvements in ergonomics. Take a look at this example from tokio's front page. Do you see anything wrong?
fn main() {
// Create the event loop that will drive this server
let mut core = Core::new().unwrap();
let handle = core.handle();
// Bind the server's socket
let addr = "127.0.0.1:12345".parse().unwrap();
let sock = TcpListener::bind(&addr, &handle).unwrap();
...
}
Elegant abstractions? check. Easy-case-made-easy? Check. But what are all these calls to unwrap()
doing here? As sidlls on Reddit put it:
My view is that sample code such as this should be as idiomatic as possible and that means providing a sample demonstrating a typical real use case. So seeing "unwrap" in this context doesn't sit well with me.
I agree! So let's do something about it. This message is serves a few purposes. First, it's an advertisement for a mentoring opportunity: I'd love to find someone who wants to work on this RFC with me (and others on the lang-team). Second, it's a vague proposal, making the case for this change and laying out a few different ways we could go about it.
What is the problem exactly?
There is a real danger, I think, that people will copy-and-paste code from these examples, which use unwrap()
, into their own functions, where unwrap()
is quite likely the wrong thing to do. Accumulated over many projects, this can lead to a lot more panicking and a lot less correct error handling. This applies not only to front-pages, but also to rustdoc documentation, where the use of unwrap()
is sadly rampant. It would be so much nicer if it were possible to write example code that used ?
instead of unwrap()
. As a side benefit, the example code would be more concise and attractive anyhow:
fn main() {
// Create the event loop that will drive this server
let mut core = Core::new()?;
let handle = core.handle();
// Bind the server's socket
let addr = "127.0.0.1:12345".parse()?;
let sock = TcpListener::bind(&addr, &handle)?;
...
}
Of course, the code above will not compile. The main()
function has a return type of ()
, but the ?
operator wants to propagate the result, and so it expects a function whose return type is Result<(), E>
for some E
(Box<Error>
would likely be a good choice, here). Of course, if I tried to change the type of main()
to match, that too will not compile:
fn main() -> Result<(), Box<Error>> {
let mut core = Core::new()?;
...
}
Now the problem is that main()
is required by the language to have return the type ()
. This makes sense, on the one hand: what would we do with the return value? But it's also an obstacle here.
But what about non-novice users? Do they benefit?
So far, I have framed this question as being all about novice users who don't know that they should be translating unwrap()
calls. But I believe solving this problem would benefit all Rust users at any skill level. If nothing else, it's annoying to have to translate unwrap()
calls when copying-and-pasting code from examples (not just examples on the front page; similar concerns apply to many bits of Rustdoc). More deeply, it just simplifies our lives if ?
is the uniform way of saying "propagate this error up higher" -- in the case of main()
, that means propagating the error up to the standard runtime, which can "handle it" by printing it out to the screen (or, perhaps, in some way that you as a user can customize, read on).
How can we solve this?
I don't have a fully developed solution to this problem. I see two basic approaches to solving it:
- Allow
main()
to return more kinds of things (in particular, results). - Change the
?
means when it is used inmain()
.
If you were interested in working on the RFC, you could help decide which one of those makes the most sense. Hint, hint. =) Anyway, let's look at them in a bit more detail.
Allow main() to return more kinds of things
We saw earlier that f main
could return a Result<(), Box<Error>>
, then we could use ?
within the body in a pretty natural way. We might generalize this concept by saying that main()
must return some type T
that defines a new trait; let's call it Terminate
:
trait Terminate {
fn terminate(value: Self);
}
The standard library (which calls main()
) will now call Terminate::terminate(main())
instead. So the terminate
method basically defines what happens when main()
terminates by returning a value of that type. For example, we would define terminate
for ()
as a no-op:
impl Terminate for () {
fn terminate(_: ()) { }
}
The standard library could define Terminate
for Result<(), Box<Error>>
as something like:
impl Terminate for Result<(), Box<Error>> {
fn terminate(error: Self) {
error.unwrap();
}
}
Users could, if they wanted, define their own terminating types. (This, combined with the ?
operator supporting a customizable trait, might allow for people to more readily customize their error handling, as well). This same trait could be used when spawning threads, so that thread::spawn()
could be updated to take a clsoure which returns some type T
where T: Terminate
.
This would require defining the trait Terminate
(probably in libcore) and making it a lang-item (so that the compiler can check that main()
returns a type which satisfies it). But it's otherwise a fairly simple change.
One downside is that our example code now needs a more complex signature for main()
. It'd be nice to be able to write something more concise -- or maybe just fn main()
, as we write now. That brings us to the next thought.
We could also change how the question mark operator works
UPDATE: Everybody agrees this is a bad idea. Let's not do it. =)
I'm not sure if I like this idea, but it's also a possibility to say that ?
, if used in the main()
function (which is well-known to rustc
) and outside the scope of any catch
, would desugar differently. Instead of desugaring to something which returns the error, it would desugar to something that invokes a handler in the standard library:
// <expr>? becomes:
match <expr> {
Ok(v) => v,
Err(e) => ::std::unhandled_error(e), // or whatever
}
Where unhandled_error()
probably looks like:
fn unhandled_error<E: Error>(e: E) { ... }
The upside of this is that the return type of main()
can remain ()
. The downside is that the return of main()
remains ()
. In other words, the example code becomes less representative of what users would have to write in their own code, since unless they too are copying-and-pasting this example code into main()
, their function would need (and want) a Result
return type. So I'm inclined to say this is a bad alternative. But I thought I'd throw it out there.
Conclusions
We should solve this problem. It is eminently solvable and will benefit everyone, in some small way. Is anyone interested?
As far as the proposals in this message, the first proposal in this message is probably better: we can consider addressing the wordy return type separately -- at worst with a type alias in the standard library, e.g., std::Fallible<()> -- or maybe it is not such a problem. We should also look into how to make rustdoc permit ?
(if it doesn't already; that problem seems a bit easier).