The ? operator will be harmful to Rust

I’m totally on board with the conclusion that we need to implement this new functionality and use it widely to gain experience and data about its usefulness and ergonomics.

The only think I dislike about ? is that with it we have one strong argument less in favor of do-notation. But even if we had do-notation, having something as concise as ? for propagating errors properly would be nice.

Just compare the code you would need to write manually without using try! to handle errors, and then compare the try! code with code that uses ?. All the cruft goes away. That means a lot happens implicitly, but that lot is pure cruft that is unreadable otherwise. Having to read, maintain, and write that cruft, makes ? completely worth it.

1 Like

Having to read, maintain, and write that cruft, makes ? completely worth it.

It's more important than that: Rust needs to make .unwrap() really, really fucking unpopular if it wants to have a good ecosystem. There are plenty of people that will and have to disable unwinding which means that any library that panics hard is going to become a massive liability. We should engrain into users the idea that .unwrap() is terrifying. The way to do that, is to make the alternatives easier.

? is the first good step we have towards that goal. So even if it's great to have endless bikeshedding about the exact details about it, we should not lose track of the overall goal that is to have a good story on how errors are supposed to be handled. There is more to the story (like debug information in form of backtraces and simplifying error interoperability) we need to work on collectively as a community. The longer we already spend discussing the easy bits will just delay all the other stuff that needs to be considered.

I will say that I don't really understand how ? is a huge improvement over try!

let event : EventInfo = self.request(Method::Post, &dsn.get_submit_url())?
    .with_header("X-Sentry-Auth", &dsn.get_auth_header(event.timestamp))?
    .with_json_body(&event)?
    .send()?.convert()?;

vs

let event : EventInfo = try!(try!(try!(try!(try!(self.request(Method::Post, &dsn.get_submit_url()))
    .with_header("X-Sentry-Auth", &dsn.get_auth_header(event.timestamp)))
    .with_json_body(&event))
    .send()).convert());
30 Likes

I can't :+1: your whole comment enough. For what it's worth, one of the reasons I was in favor of abort-on-panic semantics was to further discourage the use of panics for fine-grained recovery in libraries.

In particular, I wanted (and still want) to avoid the result in the Go community, which initially talked about panic in much the same way as we do ("game over man" from Why Go gets exceptions right by Dave Cheney), but has since come to accept more fine-grained usage (from the Go wiki):

Within a package, however, especially if there are deeply nested calls to non-exported functions, it can be useful (and improve readability) to use panic to indicate error conditions which should be translated into error for the calling function.

Making abort-on-panic a thing in Rust makes it much harder for this idiom to develop, and I personally agree with you strongly that we need to make error-handling via Result ergonomic enough so that "deeply nested calls to non-exported functions" don't feel like a good reason to try to circumvent the usual error handling mechanism.

There is still more work to be done here, especially around how library Results are built, and there has been some user-land work (for example, error-chain's macro to generate error types), but the situation once we have ? is approaching the optimal balance of propagation ergonomics and explicitness.

For what it's worth, I think it's fine for application authors who know they have not enabled abort-on-panic to use unwrap() to some coarse-grained isolation boundary (like one with Send, which avoids virtually all leakage of data from inside the isolation boundary, or one that knows how to correctly poison data that was shared with the isolation boundary, which makes sense in FFI scenarios).

8 Likes

This has also been my experience. The ? operator is fine (especially with a nice syntax-highlighting tool), it's no harder to miss than closure notation, and it does its job precisely. Having used ? extensively in Rust, I find going back to try! tedious. Moreover, try! and unwrap() both add a lot of line noise that ? does not, and they're basically accepted practices in terms of control flow subversion that ? simply codifies.

The largest problem with do notation in Rust has to do with ownership and moved values, not syntax.

3 Likes

This isn't the place to discuss this—you can open an RFC if you like. But I can guarantee you that this will not be popular.

I'd like to make one note here: this post exemplifies the strange ideas around unwinding that are often found in the Rust community, due to a misunderstanding of the points made in debates about it. In fact, SJLJ unwinding is inferior to DWARF unwinding in essentially every respect. The points that the original critics of unwinding made had nothing to do with the precise mechanism of unwinding. Rather they had to do with what pervasive unwinding did to the control flow graph of programs. In particular, the issue at stake was that unwinding causes landing pads to be generated at most call sites containing calls to destructors and a resume function, which collectively constitute a lot of extra code. The extra control flow edges also make certain optimizations harder to do.

SJLJ unwinding would inherit all of the above problems. In addition to those problems, it would make the no-unwind case slower, by requiring a complete copy of the register state to be saved before most calls. So it'd be a pure loss compared to today's -C panic=unwind. It's no substitute for -C panic=abort, because if implemented it wouldn't address any of the problems that resulted in the addition of -C panic=abort in the first place.

14 Likes

I think any conversation about reverting the ? RFC that isn’t based on actual experience using the feature on unstable is unproductive. We’ve already had the discussion about hypotheticals and design during the RFC, and one thing that seemed clear from that conversation was that the ‘PL design’ level disagreement about this feature was irreconcilable. We’re not going to be changing any minds circling around this again and again.

So far, we have heard some comments about experience using ? in this thread (both positive and negative), and those comments have been the most interesting to me. Hopefully these comments aren’t lost in the noise as we try to determine how to move forward regarding this feature. Here’s mine:

What I love about ? is that it does not wrap the expression it applies to. Among other advantages of this that were discussed hypothetically already (like that chaining methods is nicer), what I really appreciate is how much more ergonomic it makes this experience that I commonly have:

  1. I need to call this method, I start typingfoo.bar_baz(quux...
  2. I realize that this method returns a result and I want to “throw” it.
  3. With ?, I just add a ? at the end, no cursor navigation needed. With try!, I have to navigate back to the beginning of the expression to wrap it in a try!.

This is a significantly more fluid experience, and it saves me a small but noticable amount of time and focus all the time as I am writing Rust. To me, these kinds of small but constant improvements to useability are signals of excellent syntax.

13 Likes

[quote=“burntsushi, post:39, topic:3882”] Either way, I think the community reached a consensus that we should adopt ?[/quote]

No, the people who make language decisions decided that ? should be added. Let’s not mischaracterize that as a “community consensus”. There was no community consensus. Further, it wasn’t a decision made based on any clear evidence. Certain people liked it and they were the ones making the decision so it was added.

[quote=“burntsushi, post:39, topic:3882”] […] Rehashing the RFC really won’t do us much good unless there’s some compelling new evidence (like, for example, lots of people tried it and hated it, but I’m not seeing that).[/quote]

First, lots of people have given up debating it, like me, because the debate is pointless, since nothing is going to change as a result of the debate. We can’t simultaneously tell people to stop arguing against it and then say that there isn’t enough volume of complaint against it to warrant removing it.

Secondly, lots of people avoid using it. I do not use it and I don’t plan to accept its use in PRs for my projects, for example.

Ideally, somebody would add a lint for it to the compiler so that projects can opt into rejecting it by default.

3 Likes

Do you plan to avoid using try! as well?

Honest question: how do you know you hate it when you haven't tried it? Or have you, in which case, what made you hate it more than try!?

It made all the code I've seen switch from try! to it so much nicer.
It's trivial to make a good editor highlight all uses of it (unlike many exception systems), but it doesn't get in the way (like manual error handling).

So personally, I can't see where you're coming from, not to say that there isn't a valid concern, but that if there is, it's hiding under something that looks more like FUD.

Concerns about confusion with a similar-looking operator in other languages, rather than with the feature itself, are more interesting, but ? isn't the first such case either.
Macro invocation syntax in Rust is similar to both calling functions ending in ! in some languages (Ruby is one of them IIRC) and passing generic type parameters in D, for example.

As for "safe navigation", it's generally associated with "nullable values", so in Rust it would mean ? working with Option.
Whenever that gets implemented, catch should also be implemented to serve the similar usecases.

Nobody has had experience using ? with Option and perhaps the current design that works for Result won't be as ergonomic for Option-manipulating code, which would suggest we shouldn't stabilize ? at all before we've had some experience with Option.

3 Likes

If you submit a PR to ring or webpki or untrusted that improves the error handling, and I'll give you an honest and fair assessment of the attempted improvement there.

Again, my point is that if we tell people "stop debating this" we can't also say "Since there's not a lot of objection there must be consensus." It's a no-win game for anybody that actually disagrees; if they object, they get accused of dragging on the debate after it ended and/or get accused of creating FUD; if they don't keep objecting then they're considered part of a consensus that they aren't actually part of.

6 Likes

In a consensus process involving a distributed community, there has to be some kind of way to arrive at a conclusion once the debate has been fully aired and no new information is being presented.

The existence of an objection is not sufficient to block coming to a rough community consensus, because that would reject the wishes of the vast majority of participants and enshrine status-quo bias as the governance policy of the project. I don't think I can remember a single significant change to any reasonably-used programming language that didn't have some objections at the end of the day.

If you find yourself in the small minority in a consensus process (which I, quite often do, see TC39 :wink:), you have two options: continue to fight the good fight by throwing your body in front of the decision and try to persuade some people of your opinion, or concede that you're not ready to die on the hill of that decision.

Stopping to argue doesn't mean you have come to agree with the decision, but it does mean you aren't willing to continue to put up a fight anymore. And yes, the effort required to sustain an objection is inversely proportional to the size of the participant group that believes in the objection.

Consensus doesn't mean everyone agrees; that would be impossible. It simply means that all of the participants have decided that they can live with the particular decision, because the various costs to each position have been adequately minimized.

14 Likes

I think it's worth noting a few things here. (I'm not sure that you disagree with any of what I'm going to say, but there's a bit of a sinister insinuation in your comment that I'd like to address.)

Fundamentally, a project needs decision-makers, especially for cases where there are contentious bikesheds without a clear consensus across the entire community. (And it should be noted that there are very few major decisions that don't have at least some strong naysayers in the community; such is the nature of tradeoffs). Rust's model here is the open RFC process to gather arguments, constraints and tradeoffs, with the decision being reached by consensus amongst the established subteam, based on their evaluation of the tradeoffs. We're attempting to strike a balance between hearing from everyone, while having some structure for actually reaching a decision and making sure that decisions over time are coherent with an overall design and set of core values.

In terms of evidence, syntax is one of the thorniest topics, which is part of why its so prone to bikeshedding. When feasible, we do gather evidence, but this also takes time and often isn't enough to sway the entire community anyway.

But one crucial way we gather evidence for everything we do is through the stabilization process, shipping on nightly first to gather real-world experience. While even that will never make a slam-dunk case, it's clear that many people have had a strongly positive experience using the feature (though I'm sure some have had a negative one).

In the end, for Rust to continue evolving, we have to be able to make decisions. When it comes to language evolution, we've tended to make those decisions rather slowly (especially for stabilization); probably too slowly. We try to be as open as possible, but that doesn't mean we can please everyone. And I think for this particular issue, there's been a lot of passion on all sides, over a very long period of time, which is why many are expressing sadness at the whole debate being reopened again.

As an aside, the lang team plans to bring the ? syntax up for final comment period during the next (6 week cycle), given that we now have a pretty clear idea of where the Carrier ideas are going and how to future-proof against them.

13 Likes

Off topic, but this is essentially a special case of bare function calls versus methods; if the syntax were foo.try!(), as has been suggested before, it would still be more verbose than ? but cursor navigation wouldn't be a problem.

I think ? itself is fine, but there are many other macros like try! which work sort of like function calls, but would, if they were normal calls, probably be more idiomatically implemented as methods. So even though it's been proposed in the past as an alternative to ?, I think it's worth considering some kind of method macro syntax, where some_expr.bar!(arg) desugars to something like bar!(some_expr, arg), as a separate feature. Since Rust macros are part of the AST already, I think this could be done without increasing parsing complexity too much, as long as the callee were limited to being an expression rather than an arbitrary set of tokens (which would have odd interactions with operator precedence and such).

2 Likes

I generally agree and have proposed it from time to time. My main concern is that the method style implies some kind of type-based dispatch, which this feature (as usually proposed), would not have.

[quote=“aturon, post:53, topic:3882”] I think it’s worth noting a few things here. (I’m not sure that you disagree with any of what I’m going to say, but there’s a bit of a sinister insinuation in your comment that I’d like to address.) [/quote]

I agree we’re saying approximately the same thing. Please send me a private email about the “sinister insinuation” you perceived, but which wasn’t intended and isn’t present, so I can address it: brian@briansmith.org.

I'm sorry; it wasn't really fair of me to phrase things that way. As you say, it's a matter of perception, and I'm certainly a biased perceiver here! In any case, I primarily wanted to highlight the important contours and legitimacy of the process.

As somebody who was heavily against ? when it was proposed, I have actually come to like it. I am still critical of the catch aspect, but otherwise the sugar is very nice and convinient.

It might not match entirely with other languages, but despite of that, it represents the same thing, i.e. so form of recoverable error handling.

I don’t think it is a goal at all to mirror other languages’ behavior exactly.

11 Likes

From re-reading the whole discussion again I cannot avoid having the feeling that those who have used the feature are in favor of stabilizing it (with some tweaks like Carrier), while those against it are just repeating the same arguments given in the RFC without adding any new arguments to the discussion.

The feature is implemented on nightly, so if you think that the feature is “bad” in some way, it is now easier than ever to just show a working example. Please do this!

I can understand that we are all persons, and we attach feelings to thing, but “I did not want ? so I will not use it” is not really a technical argument.

The RFC is accepted, this is just how things are, but this does not necessarily mean that it will be stabilized. It just means that it will get implemented on nightly so that we can gain experience with it. Remember that most of the rust features implemented this way have actually been removed from the language and never stabilized! This trend is no longer true since 1.0, but it does not mean that it cannot happen again.

So please, if you think that ? is a bad idea, come up with working examples that show why. In the worst case it might make ? a bit better, and who knows, maybe it even leads to a solution that is good for all. Surprising things have happened in the past when people actually go and try things (e.g. scoped thread!).

We really do need people actively trying their best to break nightly features.

6 Likes

Wouldn't foo.try!() syntax just be syntax sugar for try!(foo)?

1 Like