rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Add "spaceship" operator.

Open bjoernager opened this issue 10 months ago • 17 comments

Rust should have a spaceship operator <=> for cleaner three-way comparisons.

Rendered.

bjoernager avatar Feb 21 '25 17:02 bjoernager

My feeling is that this doesn't add very much extra expressivity, especially considering the high cost of adding a new operator to the language.

peterjoel avatar Feb 21 '25 18:02 peterjoel

My feeling is that this doesn't add very much extra expressivity, especially considering the high cost of adding a new operator to the language.

The goal of this isn't to increase expressiveness (per se); it's just to add a shorthand for an existing feature. :)

bjoernager avatar Feb 21 '25 19:02 bjoernager

This RFC lacks motivation other than it making code a bit shorter. We don't just add operators based on that, if we did we'd have hundreds of operators. You should either add a lot more motivation on why exactly partial_cmp deserves an operator and many other more common functions don't, or close the RFC.

Noratrieb avatar Feb 22 '25 15:02 Noratrieb

In addition to the lack of motivation, the other big issue with this operator is I don't think that it's actually very obvious whether <=> should do PartialOrd or Ord.

To me, having to filter out None values on an operator like this feels extremely tedious since, for a large majority of the cases where I would use it (strings and integers), I would effectively be adding a None => unreachable!() branch. And even for floats, I would probably prefer using total_cmp instead of partial_cmp, which would not work with the operator since it's a separate function, not a trait impl.

And there's also the issue that the operator returns an enum that is not in the prelude, and so we'd have to manually import it, even though the operator itself is usable in the prelude. That feels at least kind of bad, and I'd assume that adding cmp::Ordering to the prelude is completely off the table due to the fact that even the standard library exports multiple Ordering types, and there are countless libraries that might have types named Less, Equal, and/or Greater that would conflict with importing the variants directly.

While I think it's cool, ultimately, there are too many unanswered design questions that the RFC doesn't really try to answer. While I think that the desire to get involved is always commendable, it really doesn't feel like you've properly thought this feature out enough for it to be viable as an RFC right now.

Note that this doesn't mean that I'd always be against a comparison operator, but that I feel that a substantial amount of design work would have to be done first that has not yet been done.

clarfonthey avatar Feb 22 '25 19:02 clarfonthey

You all have very compelling arguments. I will yield in that this proposal may not necessarily be mature enough for serious consideration as-is (other than the question of whether a <=> operator is something we even want – never mind its precise semantics). In any case, the most reasonable thing, I believe, would be to put it on hold until something more concrete has been formulated. It seems you also share this view, so I will mark it as a draft for now.

bjoernager avatar Feb 22 '25 20:02 bjoernager

Note that MIR actually does have this as a BinOp, which will soon lower to a relatively new LLVM intrinsic.

One possibility: add both <=> and <==>, where one is cmp and the other is partial_cmp (dunno which is which), or other possibilities like <=?> (probably not <≟>, even if that's fun).

Or maybe it could use a "if it's Ord for all lifetimes, it's cmp, otherwise it's partial_cmp" magic definition?

Also, I don't think adding <=> would need to affect tokenization at all. We could say that the parser just looks at the existing <= and > (or <= and =>) tokens when matching the operator. Which would make it much easier to do than if it needed to add a new <=> token -- and in fact the parser is already parsing <=> today using that approach!

https://github.com/rust-lang/rust/blob/07697360aee0cebcb4e304236ba1884d8dde5469/compiler/rustc_parse/src/parser/expr.rs#L233-L245


That said, last I did a straw poll about whether people were interested in having it, the reception was lukewarm-to-negative. So I suspect it's unlikely to land, even if I'm vaguely in favour myself.

It would be nice in the derives particularly to have it avoid the extra dereference-and-inlining on primitives today that < and friends avoid by being primitive operators. But it's also true that that's not enough of a reason to add a whole publicly-visible operator.

scottmcm avatar Feb 23 '25 02:02 scottmcm

<=?> for partial_cmp seems... "not entirely unreasonable"

Lokathor avatar Feb 23 '25 02:02 Lokathor

<=?> for partial_cmp seems... "not entirely unreasonable"

as a non-standard operator among other programming languages it would be hard to remember the spelling whether it should be <=?> or <?=> though :upside_down_face:

kennytm avatar Feb 23 '25 04:02 kennytm

The compiler would just tell you if you got it wrong, you'd learn in like 10 minutes probably.

also it "obviously" goes at the end because in expressions it's always a postfix operator, let x = some_fn()?;

Lokathor avatar Feb 23 '25 05:02 Lokathor

also it "obviously" goes at the end because in expressions it's always a postfix operator, let x = some_fn()?;

that would suggest the operator should be named <=>?.

also the postfix ? "unwraps" an x: Option<T> to produce a x?: T, but for this operator it's sort-of the opposite that "wraps" from (a <=> b): Ordering to (a <=?> b): Option<Ordering>.

~~so it should be spelled <¿=>.~~

kennytm avatar Feb 23 '25 06:02 kennytm

i see what you're saying but also emotionally i want the ? to go inside the < > pair, you know what i mean?

Lokathor avatar Feb 23 '25 06:02 Lokathor

also it "obviously" goes at the end because in expressions it's always a postfix operator, let x = some_fn()?;

that would suggest the operator should be named <=>?.

also the postfix ? "unwraps" an x: Option<T> to produce a x?: T, but for this operator it's sort-of the opposite that "wraps" from (a <=> b): Ordering to (a <=?> b): Option<Ordering>.

~so it should be spelled <¿=>.~

This is actually why, although a similar thought to that mentioned came to mind, I actually didn't think it was a good idea after thinking about it.

It feels almost like x <=?> y would be a wrapper for (x <=> y)? which would make sense but still be very weird.

clarfonthey avatar Feb 23 '25 06:02 clarfonthey

a wrapper for (x <=> y)? which would make sense but still be very weird.

That's kinda fun, actually. x <=> y is Ord::cmp(&x, &y) and x <=>? y is PartialOrd::partial_cmp(&x, &y)? -- then both "operators" give you Ordering.

(I'm not convinced it's actually good, though.)

scottmcm avatar Feb 23 '25 07:02 scottmcm

Shouldn't it be <?=> for similarity with !=?

bjoernager avatar Feb 23 '25 12:02 bjoernager

What about <~> for partial_cmp and <=> for cmp? Having a ? makes it seem like we're propagating the option while ~ has a much more... "partial" feeling to me. Downside is ~ is annoying to type :(

nohenry avatar Mar 01 '25 01:03 nohenry

For fun, I tried implementing this since it's pretty easy.

Turns out that having a <=> operator used by the Ord derive makes the derives test case about 2% faster to compile. Could plausibly double that with one for PartialOrd too.

EDIT: as I already said above, no, I don't simplifying the derive alone is reason to add the operator.

scottmcm avatar Mar 01 '25 20:03 scottmcm

That's a stress test, none of the actual crates (which I'm sure contain a reasonable amount of derives) actually saw any measurable changes, so I think we can safely say that this has no actual compile time effect (not that adding an operator to the language could be justified by derive compile times).

Noratrieb avatar Mar 01 '25 21:03 Noratrieb

One possible motivation for adding a spaceship operator would be to reduce the differences between (modern) C++ and Rust where it's mostly inconsequential. A C++ developer trying Rust for the first time might reach for the <=> operator, and it could be helpful for their transition.

However, Rust already has a customised compiler error when using <=>:

error: invalid comparison operator `<=>`
 --> src/main.rs:2:13
  |
2 |     match 1 <=> 2 {
  |             ^^^ `<=>` is not a valid comparison operator, use `std::cmp::Ordering`

error: could not compile `playground` (bin "playground") due to 1 previous error

So for a C++ developer, they'll try to use <=>, be told to use std::cmp::Ordering, then see an example using a.cmp(&b) instead. Considering a <=> b is only one character shorted than a.cmp(&b), and Rust doesn't support function overloading (which is how C++ handles partial vs strong vs weak ordering), I think this RFC should be closed. It's a nice idea, but C++ only pursued the new operator because each comparison had to be implemented individually, whereas Rust implements all comparisons in 2 traits (PartialEq and PartialOrd). <=> solves a problem Rust just doesn't have.

bushrat011899 avatar Oct 14 '25 04:10 bushrat011899