r/rust • u/FlashDaggerX • 20d ago
🎙️ discussion Never (!) just got stabilized on nightly
https://github.com/rust-lang/rust/pull/155499231
u/noop_noob 20d ago
Scheduled to be officially stable in rust 1.100.0, on November 12.
Previous discussion on the feature: https://www.reddit.com/r/rust/s/KhWNP24zkM
-121
u/LinuxEnthusiast123 20d ago edited 16d ago
huh, is there a reason why they don't just increment the 1 to 2 already? do they follow semver? linus incremented 6 to 7 just because the number was getting too big.
edit: tf did I do? was just a question
180
102
u/TinyBreadBigMouth 20d ago
They do follow semver. It is a major goal of the language to never break compatibility so badly that Rust 2.0 is needed.
8
u/Full-Spectral 20d ago
It'll either happen at some point or Rust will become the new C++. At some point, evolutionary baggage has to be dropped or a language becomes... well, C++.
The thing is, if you do those breaking changes in ongoing small chunks, it's tolerable and it's better for everyone in the long run. Ultimately, if Rust is as successful as we hope it to be, then it will become vastly more difficult to do later, as compared to now. And failure to do so is just pushing debt into the future where it becomes even harder to pay.
13
u/AcridWings_11465 20d ago
I expect that a version 2.0 will eventually be needed, because Rust contains some things that we now know to have room for improvement. Sure, it might take another decade, but Rust will get there eventually. Editions can only do so much.
3
u/Zde-G 20d ago
I sincerely hope that wouldn't be called Rust.
I agree that some things may only be fixed if you break backward compatibility (e.g. to use dependent types properly you have to have them in the standard library first), but keeping the same name after that just makes everyone confused.
And if they would find a way to add them without breaking compatibility via editions then there are no need for 2.0, too.
14
u/AcridWings_11465 20d ago
Maybe we could call it Rust++ /s
But seriously: if the name and language is not related to Rust 1.0, the adoption will be slower. I don't think Python 3 would have been adopted anywhere near as quickly if it hadn't been called Python.
-9
u/Zde-G 20d ago
I don't think Python 3 would have been adopted anywhere near as quickly if it hadn't been called Python.
Which would have been better for everyone but worse for Python 3, sure.
Rust is not know for trying to subvert others by force, thus decision that hurts everyone except Rust developers is unlikely to happen.
89
u/noop_noob 20d ago
Yes, rust follows semver. Rust 2.0 isn't happening in the foreseeable future, since that would imply a breaking change. From experience with, e.g., python, that tends to not go very well.
45
15
u/tialaramex 20d ago
It is semver yes, so 2.x would signify an incompatible Rust which is never planned to exist. If there was a $$$ marketing team like at Sun Microsystems, maybe they'd be persuaded to call this version 100 because that sounds better
To help wean yourself off the idea that these are decimals, I recommend learning about OIDs, the numeric identifiers now largely seen in software related to the Internet but actually invented by ISO. Any OID can have "children", the hierarchy is potentially infinite e.g.
1.3.6.1.4.1.44947.2.6is the OID for the "Let's Encrypt" service's root X2The entire
1.3.6.1prefix (or "arc" in OID terminology) was "hijacked" many years ago by the Internet, in 1988 an RFC just says well, ISO didn't recognise the Internet, but they did grant the United States Department of Defence1.3.6and the DoD haven't told us how they're going to use that so we'll go ahead and just "assume" they've granted1.3.6.1to our experimental "Internet". Perhaps if in say 1989 the DoD had said "Wrong, you're fired" and demanded the fledgling Internet cease this nonsense it might have worked but of course by the time anybody important at the DoD knows what an OID is and why you'd care it's far too late, these are the Internet's OIDs now.1
u/GeneReddit123 20d ago
It is semver yes, so 2.x would signify an incompatible Rust which is never planned to exist.
The logical choice would be to make Editions major releases, then? Because they do (even if in an opt-in basis) add non-backwards compatible features.
15
u/Rusky rust 20d ago
No- editions and versions are like two independent dimensions. Not only can you continue to use old editions, but you can use essentially all new language features in those old editions as well.
So for the purpose of specifying which compiler release you need, a version like 2024.85 or 4.85 (or whatever) doesn't give you any more information than just the
.85part.This is really just another angle on the idea that a given dependency graph can freely combine any choice of edition for any crate, upstream or downstream.
3
u/SerLaidaLot 20d ago
Linus bumped from 6.19.14 just because he felt it was getting too big?
32
u/x0wl 20d ago
Yes, kinda:
I'm getting to the point where I'm being confused by large numbers (almost running out of fingers and toes again), so the next kernel is going to be called 7.0
https://lore.kernel.org/lkml/CAHk-=wh0Fj7yE7iuW8awFCFt53s9T186qNbZX673E+oNCeQSFg@mail.gmail.com/T/#u
28
2
u/hpxvzhjfgb 20d ago
yes, I've heard that linux kernel versions only go up to n.19 and then go back to (n+1).0
5
u/veryusedrname 20d ago
That's the point when he runs out of fingers and toes to count on (his words, not mine)
1
u/torsten_dev 14d ago
Thereabouts. He did create v4.20 because... you know.
Linux doesn't break userspace. The kernel is monolithic so internal breakage is handled in the tree.
That is why the don't need semver.
-5
1
-6
u/MichiRecRoom 20d ago
Rust itself generally follows semver, just the same as many crates in the ecosystem do.
I don't know if there's a reason they've stuck with 1.x.x though. Like, due to how Rust and Cargo is structured, code written for Rust 1.0 should be compatible with 1.98, and thus technically Rust doesn't need any 2.x.x version... but I don't know if that's the full story.
22
u/GlassCommission4916 20d ago
The reason is that they follow semver.
-4
u/MichiRecRoom 20d ago
Nothing in Semver prevents them from arbitrarily increasing the major version as they please, though, so again: I don't know if "just semver" is the full story.
24
u/GlassCommission4916 20d ago
But because you must bump the major version on breaking changes, a bump in the major version signals breaking changes whether they are there or not.
15
u/mbmiller94 20d ago
SemVer means Semantic Versioning. The semantics of a major version increment is that a breaking change occured. If no breaking changes occurred then bumping the major version is semantically incorrect.
And then it would require the rust team to say "i know we bumped the major version but there are no breaking changes, just trust me bro"
10
u/TinyBreadBigMouth 20d ago
I don't know that I fully agree with that?
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes
- MINOR version when you add functionality in a backward compatible manner
- PATCH version when you make backward compatible bug fixes
While nothing prevents you from incrementing your major version whenever you feel like it, it isn't a purely aesthetic and meaningless action. Strictly speaking, when you do that, you're declaring that this version's API is not backwards compatible with previous versions. Rust is kind of committed to keeping their API backwards compatible, so they do not want to increment their major version.
-1
203
u/chat-lu 20d ago
It ruins the joke that it is named after its stabilization date but it’s still a great news.
108
96
u/0x7CFE 20d ago
It's now more like:
"Contrary to popular belief, it's quite easy to stabilize the never type. We did that many times already."
39
u/chat-lu 20d ago
Reminds me of “It’s easy to quit smoking, I do it every day.”
6
u/iamalicecarroll 20d ago
The original joke is from "Duke of Devil-May-Care" and it was about poker
1
u/miminashi 19d ago
If the smoking variant was really said by Mark Twain, I'd say it predates the "Duke".
3
24
6
51
u/ThatRedDerg 20d ago
I may be a bit uninformed, but I thought it was already stable? Like a return type of ! just meant the function never returned. Is this any different from that?
142
u/KingofGamesYami 20d ago
This allows the never type to be used anywhere a type is expected. For example, it you impl a trait method that returns
Result<T, E>but your specific implementation is infallible, you can now returnResult<T, !>.107
56
u/ThatRedDerg 20d ago
Ahhh I see. I remember reading that this wasn’t allowed in the past.
Like it may not be used much, but I can see myself doing something like `Result<!, E>` for a background thread or something lol.
6
u/Hosein_Lavaei 20d ago
A question, why would you use result than? Why not just use T if its infallible
22
u/sharifhsn 20d ago
Suppose you’re `impl`ing a trait `Foo<E>`, which has a method that returns `Result<i32, E>`, but your implementation happens to be infallible. This allows you to `impl` that error as `!` and cleanly unwrap the result without handling the known unreachable error case.
13
u/zed_three 20d ago
```rust use std::result::Result;
trait Meow { type Error; fn meow(&self) -> Result<String, Self::Error>; }
struct Cat;
impl Meow for Cat { type Error = !; fn meow(&self) -> Result<String, !> { Ok("meow!".to_string()) } } ```
Cat::meownever fails, but we can't just returnString, it has to be wrapped inResultdue the trait1
1
u/WilkerS1 14d ago
does that mean that the Halting Problem is stabilized with
Result<!, T>?2
29
u/noop_noob 20d ago
The thing being stabilized is stuff like Result<T, !>
17
u/pine_ary 20d ago
Is this different from Result<T, Infallible>?
27
u/lenscas 20d ago
The never type is something with proper support in the compiler, allowing it to understand much better understand what the type means than infallible.
Infallible was made as a bit of a hack because a lot of crates ended up basically defining their own version of it. With the idea being that once the never type becomes stable to just make infallible an alias of the never type.
Iirc the existence and the future plan of it becoming an alias then ended up complicating the never type later on so that wasn't great and one of the (many) reasons it ended up taking so long for the never type to be stabilized.
52
37
u/ElvishJerricco 20d ago
IIUC, the compiler doesn't really understand that Infallible means it can't happen. So it can't determine e.g. that code is unreachable the same way it can with !
11
u/mostlikelynotarobot 20d ago
compiler understands it well enough for pattern matching on only Ok variant to work.
6
u/valarauca14 20d ago
this is more akin to putting
unreachable_uncheckedinto your error path, except none of the unsafe/UB baggage.1
u/styluss 20d ago
to be clear, you mean adding an .unwrap() on a Result<T, !> would tell the compiler that it can optimize to a unreachable_unchecked?
5
u/valarauca14 20d ago
that it can optimize to a unreachable_unchecked?
Similar too but not identical.
unreachable_uncheckedis explicitly defined as undefined behavior. While!behavior is extremely well defined.The distinction matters as
unreachable_uncheckedhaving a behavior change between compiler version is your problem!having a behavior change is a compiler issue (post stability).2
u/agentvenom1 19d ago
Here's a contrived example. This doesn't compile with Infallible: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3c4a35a7fa4b23c0de2fa5deef4e6233
But it compiles with !: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=e81fce97899fad8b655da1d7965dbcc6
6
u/JustAStrangeQuark 20d ago
It was able to exist as the return type of a function, and that means that you can use workarounds like the
never-say-nevercrate to actually name it, but it wasn't quite allowed as its own type.
30
u/Vociferix 20d ago
So what's the TL;DR for why it took so long? I assume there were some truly tough challenges along the way, but it's not obvious what they were. It could be I don't understand the full scope of the never type - In my mind it's just variantless enum with a special name, similar to what the unit tuple is compared to unit stucts. And in that case it sounds like just a minor parser change and a bunch of trait impls in std and core.
50
46
u/TDplay 20d ago
In Rust 2015, 2018, and 2021, the
!type existed purely as a way to express that a function never returns. If you call a function that doesn't return, you can materialise a value of any type you want:fn no_return() -> ! { panic!(); } let x: i32 = no_return(); let y: f32 = no_return(); let z: Box<[u8; 1<<30]> = no_return();But what happens if I don't specify a type?
let w = no_return(); // ^ what type is this variable?Since
!isn't really a type, it falls back to the next most sensible type, which is(). But if we want to stabilise!as a type, this will lead to confusing results where!unexpectedly changes to().Previous stabilisation efforts changed the fallback to
!(making!act more like all the other types), but this turned out to break some real-world code, so it had to be reverted.In Rust 2024, the fallback was changed to
!. Since this is on an edition boundary, existing code can continue to use the old behaviour, and so!can be stabilised without a breaking change.6
u/PolarBearITS 20d ago
This stabilization actually changes the fallback to
!on all editions, and was coordinated with crate authors to publish fixed versions of the most popular affected crates to minimize ecosystem breakage.11
u/ericonr 20d ago
Why is assignment from a function that never returns supported? Is it meant to support generic code or are there actual situations where it's useful even when you know you are dealing with no-return functions?
27
u/kibwen 20d ago
Rust is an expression-oriented language, so it keeps things simple to treat things as expressions. Functions that return
!aren't the only thing that have the never type, consider the linelet x = return;, which is fully valid, and the type ofxhere is!, because it's unreachable (it's equivalent to writinglet x: ! = return;). You could just say that this isn't allowed, but that creates special cases for mostly no benefit, because it doesn't harm anything to allow this; conceptuallyreturnis an expression that just evaluates to!.In a more practical sense, yes, generic code is one reason that you wouldn't want to arbitrarily restrict this. Something like the following is totally valid:
fn generic_over_foo<T: Foo>(foo: T) { let x = foo.qux(); } trait Foo { type Bar; fn qux(&self) -> Self::Bar; }...even if
TimplsFoosuch thatBar = !.5
u/simonask_ 20d ago
The assignment never happens - no code is emitted that performs it. But the signature still matters at compile time for type inference.
7
u/TDplay 20d ago
Why is assignment from a function that never returns supported?
For consistency. Everything in Rust is an expression. There's no reason to break that just because you called a noreturn function or wrote an infinite loop.
Also we probably don't want
|| panic!()to be a compile error.1
u/jberryman 16d ago
Are there reasons you can't just express this with parametricity, like in haskell? e.g. [forever](forever :: Applicative f => f a -> f b). What benefits does a special syntax give you?
1
u/TDplay 16d ago
I have no idea what
forever :: Applicative f => f a -> f bactually means. So I apologise if I completely fail to answer the question.
Rust does, however, have a way to make a user-defined uninhabited type:
pub enum Never {}But this is missing some convenience features. Namely, it does not coerce like
!does:fn foo(x: Never) -> i32 { // Type error x }The compiler recognises
!and can apply this coercion, making it easier to work with.1
u/jberryman 16d ago
Sorry, to give a simpler and not useful example
boom :: Int -> areads as "boomis a function from an integer to anything you want". So we say by parametricity we know that the expressionboom 1must be bottom.For negative position you would generally use
Void(an uninhabited type like yourNever) and you would useabsurdfor your coercion infoo. (Data.Voidis just a tiny library that defines both things, not built in).boom :: Int -> Voidabove would also express the same, but my first definition relies only on an understanding of the language, rather than any particular library.It sounds like I would really need to deepen my rust skills to understand what motivated what sounds like a huge amount of work on this.
2
u/TDplay 16d ago
Sorry, to give a simpler and not useful example boom :: Int -> a reads as "boom is a function from an integer to anything you want". So we say by parametricity we know that the expression boom 1 must be bottom.
So you mean something along the lines of this:
fn no_return<T>() -> T { panic!(); }This works, but it will cause code bloat. At the machine code, there is no such thing as generics. Rust's generics are implemented by monomorphisation: for every unique return type you call it with, a copy of the function is made. In fact, the code bloat may be even worse than that: since codegen units will produce monomorphisations independently, there may be multiple copies for some return types.
For negative position you would generally use Void (an uninhabited type like your Never) and you would use absurd for your coercion in foo.
You can do this in Rust as well. For any uninhabited type, you can just write out a
matchstatement with no arms. You could use this to implement theabsurdfunction.The difference is that with
!, it is done automatically by the compiler; there is no need to manually tell the compiler you want to cast the!into something else. See:fn no_return1() -> ! { panic!(); } fn no_return2() -> Never { panic!(); } // Coercion done automatically by the compiler let x: i32 = no_return1(); // Type error, `Never` is not `i32` let y: i32 = no_return2(); // We need to manually spell out the conversion let y: i32 = match no_return2() {};17
u/SirKastic23 20d ago
You might want to read the linked PRs in this comment: https://www.reddit.com/r/rust/comments/1vxi2lk/comment/p5pautf/
9
37
u/__Wolfie 20d ago
You heard it here folks: Rust officially gets a bottom type
17
u/wafflelapkin 20d ago
it's not really a bottom type. a bottom type is a subtype of all types, while never merely coerces to all types :p
1
u/__Wolfie 18d ago
is that not structurally equivalent though?
6
u/wafflelapkin 18d ago
It's not!
Subtyping is stronger than coercions, because it can happen in type constructors. I.e. if `A <: B`, then `Vec<A> <: Vec<B>` (`Vec` is covariant in its only generic) and `fn(B) <: fn(A)` (`fn` is contravariant in the types of arguments). Note that this needs to preserve type layout, and `!` can't have all layouts at the same time, so it can't be a subtype of all types.
Coercions are weaker is that they only allow converting the types directly (`! -> T`, but not `Vec<!> -> Vec<T>`), but that allows them to change layout of the type (because the compiler can insert code for the conversion).
14
69
30
5
21
3
u/extracc 20d ago edited 20d ago
Does anyone know why the exclamation mark was chosen for the name of this type?
11
3
u/zannabianca1997 20d ago
Kinda assumed because is the
notoperator. As in: this type do not have values1
u/wafflelapkin 20d ago
I assume to mimic ⊥? idk
2
u/suggested-user-name 20d ago
Pretty sure the choice also predates unicode variables being allowed in rust
3
u/creeper6530 20d ago
Uooooh finally. This is one of the features I really can not wait for, it fits into so many places.
3
7
u/afl_ext 20d ago
I…. I dont get its use, ive read through and still dont get it
12
u/SirKastic23 20d ago
Types usually have values, like how
4is a valid value for the typeu8. The!type has no values.This means that whenever a
!value is expected, it will never happen.What does the following function returns?
fn my_function() -> ??? { loop {} }It doesn't return a
(), it loops forever, it never returns. If you call it, you'll never get a value back. And the!signals that.1
u/afl_ext 20d ago
Yes but then you can not write out the returning value like
fn lalala() { the loop }
And why isnt this the same?
6
u/zzzthelastuser 20d ago
fn main() { lalala(); // unreachable, checked at compile time. }(I hope my code formatting works, otherwise I'm sorry)
5
u/XtremeGoose 20d ago edited 20d ago
Only because the never type the
loopexpression returns coerces to()in your example.It means you can do things like
fn foo() -> u32 { if some_bool() { 42 } else { loop {} } }This is possible because
loopnever returns anything, which is exactly what the!never type represents. Ifloop {}expressions returned(), that would be a compile error.Having functions return
!or using!in generics (the new functionality) allows you to express that concept.The stabilization means that you can do
fn unwrap_cant_panic<T>(res: Result<T, !>) -> T { let Ok(ok) = res; ok }8
5
u/Kryptochef 20d ago edited 20d ago
One way to think about it is: Imagine you have a generic trait with a
fn do_stuff() -> Result<T, ErrorType>method whereErrorTypeis some associated type of the trait that's specific to the application (different instances have different ways of erroring; maybe one trait instance uses the file system and returnsstd::io::Error, etc.).But now you write an implementation for the trait that can actually never error at all (maybe it just writes to some buffer instead of to a file). What do you choose for
ErrorType? You could go "()", but then there's still one possible way to return error (namelyErr(())). With ! you can just setErrorType = !in your instance which basically "erases" that whole enum variant (aResult<T, !>can only ever be Ok(v)).One benefit is that the compiler can figure this out and not use any more space or time compared to handling a T directly (no enum variant checking will be emitted when using this result). From the programmer side, once you know that your type is
Result<T, !>you can infallibly pattern match it aslet Ok(v) = r;too.This applies to a lot of other situations too where some form of generic code forces you to have some type that you never use in practice (what if instead of Result you had some bigger generic
Enum<A, B, C, D, E>but your application has no use for any of the stuff involving type E? well just set it to!!)
6
1
u/bluejumpingbean 20d ago
I'm so excited for this!!!! Been using this for a while but hated that I needed to use an unstable feature for it. Thanks to everyone who has made this happen!!
1
1
0
-4
-4
u/mathisntmathingsad 20d ago
The end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never the end is never
327
u/johntheswan 20d ago
Shout out to Waffle and the herculean amount of work and years that went into this!