Stabilizing Rust's Never Type(lwn.net) |
Stabilizing Rust's Never Type(lwn.net) |
This is incorrect. The compiler has always treated `Infallible` as uninhabited, and used that fact for optimizations. The downside of its lack of compiler support is losing out on the coercions. (The article is excellent otherwise)
Yeah this statement is incorrect as well. Weird to have these minor technical inaccuracies in a relatively detailed article.
enum MyNeverType {}
...and it's uninhabited, the same way you can write the product of nothing struct MyUnitType {}
... and its size is zero.Return Result<T, !> and the compiler knows that callers never have to check the error case because by definition it can’t be constructed.
If ! can coerce to every type, why not treat it as if it implemented every trait too?
So this would risk turning a compile-time error into a runtime error.
"When is never?"
The question is more whether it's unambiguous (it is), and whether there's something else you would rather use it for (there isn't).
A simple TypeScript example:
const forever = (): never => { while (true) { // whatever } }
Nobody should get ahold of this technology.
Shut down the schools!
Get rid of all small business (to mitigate the risk).
15 days to prevent Never from destabilizing!
We’re all in this together.
Thankfully Rust is compiled so this break is at compile time, and seems simple to fix. I wonder if their could even be an auto fix for it.
My software gets done.
The idea that it would suddenly not be done because the compiler upgraded sounds like a potentially limitless amount of future work. Why would I want to invest in something that promises that?
Unless you just have an axe to grind?
Plus, LLMs have really made upgrading a codebase for a compiler or dependency update into a trivial chore, at least for the most part.
https://predr.ag/blog/some-rust-breaking-changes-do-not-requ...
"Never add anything" isn't a tenable position, and in practice the breakage hasn't been bad enough to need special treatment yet.
It’s better to plan for this in advance and use a stack where upgrades are done gracefully, rather than sticking your head in the sand and pretending it doesn’t happen.
This is not the only kind of breakage a project can experience. Trying to bring up an older project on a new platform will be a compile error (like building a project from 2018 on an Mx Mac), and updating the appropriate dependency to an appropriate version might become a chore. I have no idea how to improve the situation there to make that less painful, beyond having a simple database of crate-version+platform+rustc-version so that the toolchain could provide better/actionable messaging beyond "shit's broken".
Prior to this change most Rust developers would never have cause to ever use `!` for any reason. The only stable way to do so would be to specify the quote-unquote "return type" of divergent functions, which Rust has supported via this special-cased syntax since prehistoric days, before even Mozilla got involved. You can see it in the oldest capture of the tutorial from Jan 2012: https://web.archive.org/web/20120109041112/http://www.rust-l...
So when it came time to elevate `!` from being a special-cased return type to being a fully-fledged type, it was only natural to reuse this syntax. However, I tend to agree that, because we call it "the never type" in casual conversation, the most natural thing to do would be to just have a type alias called `Never` that we could encourage people to use instead. But that would be a perfectly backwards-compatible change that could be made at any point (as proven by the fact that the stopgap and long-stable `Infallible` type is becoming just such a type).
As a type.
"!" is in the most basic example nearly every rust developer has seen:
println!("Hello, World!");
Unless you specifically watched a presentation or read a blog post about the never type, you probably haven't seen it as a type.If the average (and nearly all new) rust developers encounters "fn bla(blub: i64) -> !" I suspect they will mostly go "huh??" (or wonder what kind of weird macro that is) until it becomes common to encounter early and gets it's own early entry in the rust book.
Compared to reading "fn blub(bla: &str) -> Never", which seems rather straight forward imo. However, "Never" might be the name of some actual Struct or Enum in various codebases.
[0]: Example: std::process::exit returns `!` https://doc.rust-lang.org/1.0.0/std/process/fn.exit.html
(There is a trick you can abuse to access the type everywhere: https://docs.rs/never-say-never/latest/never_say_never/)
I just checked, my main side project only has less than 10 things that return never. `-> Never` reads even better, imo.
And if you'd like to write `-> Never`, the nice thing about being a first-class type is that you can now just do that if you'd like, via a standard type alias: `type Never = !;`.
Never is a standard type in many languages and is at the bottom of the type hierarchy because it’s a subtype of every type. Never isn’t implicitly converted any more than `&’a A` is “implicitly converted” into a `&’b B`, where `’a` subsumes `’b`. There’s no runtime conversion because there will never be an instance of never—it represents the value of a computation that never completes by definition.
I think what you mean to say is that implicit runtime conversions are bad, not that all subtyping is bad.
Rust is not a subtyping based language, except for traits and lifetimes. So statements like never being at the bottom of the type hierarchy is irrelevant here even though it is correct. If Rust had higher rank types the never type is also (forall a. a) but still it doesn’t matter. It is simply surprising for a type to be converted implicitly according to subtyping rules other than for traits and lifetimes.
From a more category-theoretic perspective, a type A is a “subtype” of a type B when there is an embedding of A inside B. In this sense, `!` is a subtype of every type (which is its universal property). But this definition also grants you that `String` is a subtype of `BigInt`, because strings can be coded as bit sequences which can be coded in `BigInt`, which may or may not be what you expect.
From a programming languages perspective – and this is the terminology generally used in Rust – a type A is a “subtype” of a type B when `a: A` implies that `a: B`. In this sense, `!` is only a subtype of itself; although it coerces to any other type, it’s not _literally_ of that type, the coercion is just invisible in syntax. Importantly, if A is a subtype of B then `Vec<A>` is a subtype of `Vec<B>` – but `Vec<!>` is definitely not a subtype of `Vec<T>`, since they may have totally different layouts in memory (the former not allocating at all, while the latter potentially allocating).
It cannot by definition happen at runtime because the never type has no values and thus cannot be constructed under any circumstances.
Any compile time coercions that occur would convert types (or generics args of types) to !
I find it difficult to imagine any situation where that would result in a working program - only if the coerced types or references to coerced generics were not even used would it compile.
Higher level application code can benefit from this, but core libraries should forbid this statically and be prevented from even compiling or being imported should these things be enabled.
We should be able to filter crates.io by these properties, and force our own projects to abide by them.
I want nopanic, nocoerscion, maxdependencydepth, rustonly, nolinking, etc. flags.
let Ok(x) = call_that_cant_fail();Another ! makes sense to me here. Are there any cases where it doesn't work to auto-assign ! to all associated types of a ! trait impl? Associated constants might require some mechanism similar to `compile_error!()`.
We know how that looks, and it's not good. See Scala and Python.
Rust's edition system gives you the best of both worlds, with caveats. Some changes will be impossible.
To avoid those problems, you'd have to proactively disambiguate any item with its trait name, which is so un-ergonomic that people genuinely prefer the possibility of occasional breakage.
If your project is truly done, by all means ship it on a N64 cartridge. I can't say I don't have sympathy for that attitude. But don't expect this is how the world works today. And don't forget, N64 cartridges weren't built using massive amounts of dependency, so it was easier to confidently declare something "done".
And when it wakes up, it’s not like this is some massive change. It’s trivial and automatable.
That's why this compiles, even though not even match arm produces a value, which I think many Rust programmers are familiar with:
let b = match a {
0 => 123,
2 => panic!("mustn't be two"),
_ => todo!(),
};
Other expression that are evaluated as having the type "!": return, break, continue.really says a lot that someone can say this apparently absolutely seriously
Luck is not a process.
Safety critical work / security-sensitive work is completely orthogonal to the rust release cycle.
Not to mention the vast amount of professionals who don’t have root on their runners / build nodes. Putting a ticket in every 3-6 weeks for a new compiler version takes… 3-6 weeks. People give up after a while.
When I talked with the people at AWS responsible for updating both the myriad Java compilers in use as well as Rust for every project in the company, they claimed that updating Rust was always painless and didn't even appear as a blip in their radars compared to other similarly wide reaching updates.
That’s just not true. Java would permit it but then you get ArrayStoreException so this is unsound from a type system perspective. To make this sound, we need to classify each use of a type parameter to be covariant, contravariant, or invariant.
First of all, Rust isn't subject to the same soundness issue as Java precisely because of the Rust's ownership semantics. You can't produce the ArrayStoreException issue because you can't mutably alias a Vec in the first place. To be more precise, &mut T is invariant, but Vec<T> is covariant (in T).
Second of all, Rust already does classify the co/contravariant status of all of type parameters. If you've ever tried to omit a type parameter from the fields of a struct and find that you're forced to insert a "PhantomData" value, this is because the entire purpose of PhantomData is to imply what variance classification the compiler should give the type parameter.
fn unwrap<T>(t: Option<T>) -> T {
match t {
Some(foo) => foo,
None => panic!()
}
}
...and this function couldn't otherwise typecheck because it doesn't return a `T` in the `None` branch. You need coercion here.Generally languages with such polymorphism have a never type only because they don’t also support impredicative polymorphism.
fn f<T>() -> T {
loop {
println!(“Looping”);
}
}
This won’t compile. How do you express to the compiler that this is correct (because the function never completes) without a never type?Rust also doesn’t support generic function types, so a callback function returning `!` is possible, whereas one accepting a type argument and returning that type is not. You could do something clever with traits, but that requires extra work.
A lot of these things are achievable without `!`, but the compiler still needs that type internally and it pieces so nicely into the type system that I don’t see the point of bending over backwards to avoid it.
This is what the article defines as fallback. So the issue has everything to do with fallback.