The ? operator will be harmful to Rust

I’m not really sure where to bring this up, since there’s already RFCs, tracking issues, etc.

I wouldn’t be making this thread if I didn’t think this was a big deal. I’m mostly content to let the details of Rust slide, and am generally happy with the language as evolving.

However, as presented, the “?” operator promises to make Rust vastly different from other syntactically identical expressions in many other programming languages, and not even in a way that’s immediately obvious.

Ruby, C#, Swift, Groovy, Kotlin, and Perl 6 all have some form of “Safe navigation” or “safe call” operator. They’re all roughly invoked something like let f = foo()?.bar()?.baz();, mostly differring in the keyword choice for let.

Rust has proposed to add something that will look nearly identical, however it will function very differently. I know of zero other languages that do something similar. Exceptions, of course, are a related idea, but the programming language community at large has experience with them, and Rust does have panics already.

By hiding return statements in the middle of expressions, I believe Rust control flow will be very surprising to casual users of basically every other programming language. Since Rust is, at least today, a niche language, that is effectively all its users. I didn’t see any discussion on the RFCs on how this feature compares with other programming languages, and that concerns me. Rust does not exist in a vacuum.

I think having a safe call operator would make a lot of sense in Rust. In fact, let f = try!( foo().?bar()?.baz() ); could reduce noise significantly from what we have today, and replacing try!() with syntax could make a lot of sense. But tacking a question mark on the end is way too subtle.

Having ? hide a return, I believe, will irreparably make Rust hard to read and learn.

19 Likes

Yeah, I kinda agree.

I agree 100%. Having worked in many other languages, I was completely confused by ? being used for try! instead of option.

1 Like

This feature was extensively discussed on the associated RFC. If you’d like to reopen discussion on the topic I’d recommend citing specific comments from that RFC with technical rationale as to why.

9 Likes

The best way to think about the mechanism proposed here is that it works like checked exceptions in programming languages that have them.

Panics aren't meant to be thought of as analogous to exceptions in Rust, but rather as analogous to abort, with some ability to recover at the application level at a very coarse-grained way if the application is configured to support recovering from panics.

A Result in Rust is a reified checked exception, and because it's reified, it can be:

  • treated as a value: used with methods like .map, and_then, or by pattern matching the Ok and Err variants.
  • immediately propagated in a lightweight way (using try!, or, in the proposal discussed here, via ?).

An on-by-default lint in Rust (must_use) prevents silently dropping the error by accident.

In my experience, these characteristics (and notably, rejecting the use of out-of-band panics to communicate handle-able errors) results in significantly more robust code than languages with unchecked exceptions.

The ? operator (and the paired -> SomeResult) is the cheapest syntax that provides the syntactic convenience of exceptions while retaining both explicit propagation and the ability to conveniently work with exceptions as values.

To some extent, it doubles down on Result as a key construct in Rust (as opposed to "just another type"), which, I believe, gives us the ability to teach ? as more of a checked exceptions mechanism than a macro that expands to return.

(If we end up being forced to teach ? as a macro that expands to a match + coercion + return, I become very sympathetic to the concerns in this thread)

21 Likes

I think your argument can be separated into two distinct points:

  1. Rust’s ? is confusing because it works differently from similar syntax in other languages.

  2. Rust’s ? is confusing because hiding returns in the middle of expressions is inherently confusing.

Both points are valid, but I have counterpoints to each:

  1. Today, safe navigation using ?. is far from ubiquitously known. Ruby’s safe navigation operator is new and uses a different symbol; C#'s safe navigation operator is only a year old or so; Swift itself is two years old; and none of Groovy, Kotlin and Perl 6 have particularly wide adoption. (However, as time passes, knowledge of ?.-as-safe-navigation is likely to spread more than that of Rust’s version.)

    Also relevant to familiarity: Rust’s ? has a fairly close analogue in Swift, it just uses different syntax, try foo() vs. foo()?.

  2. It’s worth noting that 5 of the 6 languages you mentioned, as well as many other languages people might be moving to Rust from, have non-checked exceptions, so every function call can be an implicit return.

22 Likes

Indeed. The ? operator in Rust indicates the handful of places that can produce implicit returns by making those returns explicit in the language and manually propagated.

For example, if you look at the spec for JavaScript, most operations produce a completion (which can either be a "normal" completion or a "throw" completion).

The spec has a macro called ReturnIfAbrupt:

Algorithms steps that say

1. ReturnIfAbrupt(argument).

mean the same thing as:

1. If argument is an abrupt completion, return argument.
2. Else if argument is a Completion Record, let argument be argument.[[value]].

Recent versions of the JavaScript spec have added a ? prefix operator:

Abstract operations referenced using the functional application style
and the method application style that are prefixed by `?` indicate that
ReturnIfAbrupt should be applied to the resulting Completion Record.

Yes, Completion Record is basically Result, and ReturnIfAbrupt is basically try!.

In other words, virtually all operations in JavaScript produce Result, and the language semantics add ? to every expression to propagate errors. In Rust, since many fewer expressions can produce errors, the explicitness makes it easy to handle errors robustly.

4 Likes
  • Ruby uses &. because name? is an identifier. If Rust did not use name! for macros we could use ! instead, although this could also be confusing since in Swift it means .unwrap().

  • RFC 204 and 243 are “only 2 years old”. I don’t think that a syntax being relatively new is a good counterpoint.

Maybe so, but the mnemonic with && short-circuiting is great and I'm happy Ruby ended up with &.. In any event, it means the syntax isn't universal (additionally, Perl 6 uses .?, which more strongly indicates a separate token vs. a suffix operator).

1 Like

Out of curiosity, what would it mean to be used for option?

Comex summarized my concerns quite well. I am almost entirely concerned about the first point in that summary, rather than the second.

I don’t buy the argument that the syntax is fine because the ?. operator is relatively unknown. If we want Rust to be a language that lasts more than the next few years, that won’t stay true. Rust’s ? operator will surely be even less well known, and by differing from the PL community norms, the language will forever have different behaviour from its peers. That adds cognitive load.

2 Likes

The prefix ? is only a convention used to describe the algorithms, alongside the prefix ! (meaning .unwrap()) (e.g. ECMAScript® 2024 Language Specification). These are not part of real JavaScript syntax.

Right. The use from the spec was just an illustration of comex's comment:

The fact that the spec uses mechanisms that are almost identical to first-class constructs in Rust just shows that the symmetry is more or less exact.

I'd think that the fact that Rust doesn't have recoverable exceptions, in comparison to its peers, would be a much larger divergence than the meaning of the ? suffix operator (which can be used without a following ., as in let bytes = read()?).

Rust's error story is probably going to end up being something that new Rust users need to understand pretty early on (along with the ownership story, and how to use Cargo), so I think the situation will be different over time than the way it feels when introduced as a new feature to existing Rust users.

9 Likes

I have mentioned this in /r/rust, but did you mean that even try! is problematic? I believe the RFC didn't discuss about the validness of early return itself on the ground that try! always has been existed. The view that non-local jumps including try!, nested break and even nested return are bad is valid, but is also clearly out of the RFC's scope.

2 Likes

IMO, Rust should admit that the error handling right now is broken and just bite the bullet and design a good checked exception system, that can be implemented through SJLJ (set-jump-long-jump) exceptions with optional DWARF unwinding where possible.

All these special macros and operators are just adding more confusion. Especially since there’s a parallel unwinding system of panics.

Otherwise, perhaps this issue should just be postponed for some time until there’s a clear consensus of what should be done.

1 Like

Roughly, given an option a, a?.b() would be equivalent to a.map(|x| x.b()).

2 Likes

I think Rust error Handling is the work of genius :slight_smile: And and I think it currently works sufficiently well for a system language.

18 Likes

In my opinion, the difference with other language it is not a big problem since a person who learns Rust should learn pretty early the ? syntax. And people who don’t know rust enough probably don’t need the detail when they read Rust code. They should be focused on the algorithm.

I don’t see the problem with the fact that ? can return early too : the sigil and the Result return type make this pretty explicit.

4 Likes

When the feature was first mooted I was in favour of ? doing something like proposed from other languages. But I think that the feature as implemented today is actually superior in that it matches what I want to do in most situations, and in situations where it is not what I want, then usually it is because my program is designed badly, after refactoring to appease the ? semantics I nearly always have much nicer code. Ideally, we would align with other languages, but here we can do better without doing something dramatically surprising. And, frankly, we are not talking about equals or dot here, ? is actually fairly uncommon in the wider PL world.

If you think in terms of implicit returns, you are approaching the code in the wrong way. As mentioned by others, the best way to think of ? is in terms of normal and exceptional control flow. If I am interested in reading the normal control flow for a function, I ignore the ? (which is pretty easy to do). If I want to read closer and investigate where a function could possibly return, then I have to look for ! (since macros can always return), ? (obviously), and for function calls that could panic. Of all these, searching for ? is the least annoying and even then I only have to do it if the function returns a Result. To me this a refinement of explicit exceptions, and done so in a more efficient way (both at runtime and in terms of ergonomics/syntax).

18 Likes