The never type seems very useful in various languages to either signal that a branch can never happen (the example of string -> bytestring never erroring) or to mark that a function will never return a value (and thus control) to the caller.
A simple TypeScript example:
const forever = (): never => {
while (true) {
// whatever
}
}
> After this change (and on the 2024 edition), the compiler assumes that T should be !, which doesn't implement Default, and therefore causes a compilation error.
If ! can coerce to every type, why not let it (formally) implement every trait too?
The Default trait provides a function that actually constructs the type in question. But here the ! type can never be constructed, so the only way to implement Default would be to have it panic, loop infinitely, or otherwise fail at runtime.
So this would risk turning a compile-time error into a runtime error.
In Rust, `loop {}` is an expression of type `!`, which means that from a type-theoretic perspective it is inhabited. This means that the Curry–Howard correspondence fails for Rust.
Is it obvious to rust developers that "!" would be the never type? I frequently use "never" in typescript. I could imagine using the never type frequently in rust too. I feel like a longer more human-understandable name would've been a good decision here. (feels like more rust jargon that makes the language harder to learn)
The exclamation point is also used both as the C-style negation operator and as the identifier suffix that indicates macro invocations, so it was unlikely that it would have been used for any new feature. As my sibling comment notes, this syntax for divergence is very, very old (predating even 0.1), not something that anyone newly came up with.
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 = !;`.
Even though the never type has been experimental for a while, I've seen `!` used in the docs,[0] so at this point, Rust developers are probably already aware of what it means even if they haven't used it before.
Yeah, I don't like it when languages use too many symbols for things. It's a hard line as I don't like languages where everything is a keyword (e.g. begin end vs { }), but not enough keywords and it's hard to learn and remember the language.
Why is it bad? Implicit integer conversions are generally bad because they can produce unexpected behavior at runtime and obstruct what’s really happening, but that doesn’t seem to be what’s happening here.
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.
None of this has anything bad to say about coercion or fallback, it's a consequence of the fact that Rust is an expression-oriented language which had expressions (like `loop {}`) which logically evaluated to the never type when in return position, and yet did not have the machinery in place to support it as a proper concept anywhere outside of return position, and so they chose the unit type as a relatively benign alternative in those contexts, which caused no problems whatsoever until the day came when they decided to actually implement the never type.
Let's avoid using the term "subtyping", which as you say is irrelevant here. The reason you need diverging functions to satisfy arbitrary type obligations (i.e. to coerce to any other type) is because otherwise anything as simple as `let x = Some(42); x.unwrap();` just completely fails to compile, because `unwrap` is internally just:
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.
You’re using “subtype” in two distinct, but related, senses here, and I think this should be clarified.
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).
> A is a subtype of B then `Vec<A>` is a subtype of `Vec<B>`
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.
We should be able to set at a crate level whether our code can compile with panics, implicit conversions, etc. And we should be able to blacklist dependencies and transitive dependencies that do these things. We should be able to advertise a crate's safety and attention to detail.
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.
Do you have some specific coercion in mind that you want to forbid? Unlike C, Rust is extremely tame when it comes to coercions. Forbidding coercions in general in Rust code doesn't really make sense, and I can't think of any that aren't either beneficial at best or benign at worst.
With ! the implicit conversion happens at compile time, never at runtime.
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.
My favourite never type ability is when you need to conform to a trait that returns Result but your specific implementation can never produce an error.
Return Result<T, !> and the compiler knows that callers never have to check the error case because by definition it can’t be constructed.
And if your callers are generic over the error type, and so they do have code for handling errors, the compiler won't emit this code for your result type because you've said its error type can't exist.
My first encounter will likely be the opposite, i.e. a call that will only produce a Result::Err if anything, and never a Result:Ok. I've made programs where there are a bunch of continuously running jobs that should never terminate in the happy scenario. If any of these jobs terminate, it'll be due to some sort of failure, and so Result<!, MyError> seems like a reasonable return type for such job. I think I've already used the Infallible in there but I think this being part of the language now makes the code feel more correct or clean or something like that.
> For many years, the standard library has had an `Infallible` type to work around the unstable nature of the never type. It served the same semantic purpose as the never type, but did not have any special compiler support. Therefore, code using it would be technically correct but suboptimal (such as having an extra layer of tags in an enumeration or emitting dead code), because the optimizer would not always be able to remove references to `Infallible`.
This is incorrect. The compiler has always treated `Infallible` as uninhabited, and used that fact for optimizations. (The article is excellent otherwise)
Which makes sense because you could (and indeed still will be able to do) write your own uninhabited types very easily in Rust and indeed they're optimised accordingly. Because Rust has user-defined sum types you could simply write a sum of nothing:
enum MyNeverType {}
...and it's uninhabited, the same way you can write the product of nothing
Note, to be more clear, an enum with no variants has no values of that type (can't be constructed), and a struct with no fields has exactly one value of that type
Pedantically, in practice during runtime, the empty struct type in Rust can have multiple different values. It is only in certain kinds of type theory perspectives that an empty struct has just 1 value.
struct A{}
fn main() {
let a = A{};
let a2 = A{};
println!("{:p}", &a);
println!("{:p}", &a2);
}
Will in some runs at least print two different addresses.
I wouldn't say they got it wrong exactly. Uninhabited types aren't equivalent to unit types, but in terms of Rust they're both classified as zero-size, and I suspect they were just pointing out that Rust has optimized and special-cased zero-sized types accordingly since time immemorial.
> (Note: Rust also uses exclamation marks to indicate calls to macros. The way the syntax is constructed, a place where it is valid to use the never type is not a valid place to put a macro invocation and vice versa.
Yeah this statement is incorrect as well. Weird to have these minor technical inaccuracies in a relatively detailed article.
Pedantically, the phrasing in the article is incorrect because macro invocations are permissible in type positions. But as you said, the macro invocation syntax is nevertheless not ambiguous with !-as-type (or !-as-operator for that matter).
I can see that you’ve never designed a language. The design and implementation of Rust took many years. Over that time people’s ideas and priorities changed. Even the people changed. In the early days of the language design sigils were very heavily used throughout the language. Even the language keywords were deliberately shortened as much as possible (consider ”fn“ and ”ret“, for example). Most of the sigils were eventually replaced with traits, but not all of them. ”!“ is one that survived.
It's not really like there's a budget for it, and "!" is already a type people have seen in the return position of extremely common macros (panic!, todo!, etc.), functions (std::process::exit, etc.), and expressions (return, break, continue).
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).
It results in different behavior in the compiler. You don't have to check the return type of a function in which you call a function return never. Using a different symbol gives you a hint why the code works, if you used `Never` you'd have to KNOW it works differently.
I like the pragmatic approach to backwards compatibility (accepting relatively rare breakage that's not too hard to fix) instead of requiring 100% compatibility without exceptions
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?
Everything is a trade-off. Would you never ever under any circumstances accept even trivial breakage, even if it fixes a horrible wart that costs thousands of lost hours?
I agree with your sentiment, but at some point backwards compatibility has to break. Rust handles it better than basically anything out there. If it bothers you, you should never try any other language except maybe plain, no-framework JS.
Plus, LLMs have really made upgrading a codebase for a compiler or dependency update into a trivial chore, at least for the most part.
It’s not actually true that your software gets done, that’s essentially impossible unless you’re doing some kind of performance art project targeting a defunct platform from decades ago. Operating systems and libraries change underneath you all the time, even if you’re just using Linux and glibc, and if you’re not keeping up eventually your software is the legacy code keeping people stuck on an insecure OS, like those businesses who have to keep one box running DOS because of some ancient device driver for their equipment.
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.
It’s very amusing to me that rust is trying to achieve a safety critical certification. Whatever version of the compiler that gets the cert will be cemented for the next 20 years.
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.
> 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.
Because software is rarely done. Also you will likely to write newer programs with the same but improved language that doesn't have the warts in the earlier versions. Isn't it worth paying a small price for the improvement you get in the future?
This isn't the only way a Rust stable update can break your compile. It can also happen simply because they add a symbol to the standard library, and that isn't even protected by language editions.
Oh not only to the stdlib but to any lib! I thought that was crazy when you mentioned it but after reading the post I agree it’s a pragmatic approach. By the way Java does the same! The problem can be avoided by avoiding “star imports” in both Java and Rust, and at least in Java star imports are very rarely used for this exact reason (I remember my horror when I couldn’t call a method of List that I knew existed and it turned out a “import java.awt.*;” was the reason, that was a pain to figure out before AI and convinced me to never use star imports again).
Your software is done, so why do you need to recompile it with a new version of the compiler? After all it’s done. Pin compiler version and problem solved. No limitless amount of future work.
Like Python 2 vs 3? That's what Editions were created to fix. With backwards incompatibility you will get ecosystem breakage and looming threat of future compilers not compiling your code.
The problem is that assuring no breaks ever with inference means you can't ever improve the inference algorithm nor update the stdlib. This is what triggered the time 0.35 breakage. I have a still incomplete/unmerged rustc lint to avoid the situation that caused that (a useless .into() that didn't get flagged because the clippy lint has false positives so it is not on by default) which should minimize the likelihood of that happening again (once I get off my ass and finish it, it just requires some side-quests to add more accurate tracking of cfg'd out items). This might be able to be mitigated by editions, but in practice crater helps to not need that (yet?).
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".
73 comments
[ 0.18 ms ] story [ 4.5 ms ] threadA simple TypeScript example:
const forever = (): never => { while (true) { // whatever } }
If ! can coerce to every type, why not let it (formally) implement every trait too?
So this would risk turning a compile-time error into a runtime error.
"When is never?"
https://youtube.com/watch?v=3jM4cnEVrLc
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 = !;`.
[0]: Example: std::process::exit returns `!` https://doc.rust-lang.org/1.0.0/std/process/fn.exit.html
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.
This is what the article defines as fallback. So the issue has everything to do with fallback.
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).
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.
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.
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.
Return Result<T, !> and the compiler knows that callers never have to check the error case because by definition it can’t be constructed.
This is incorrect. The compiler has always treated `Infallible` as uninhabited, and used that fact for optimizations. (The article is excellent otherwise)
What's this bullshit?
Yeah this statement is incorrect as well. Weird to have these minor technical inaccuracies in a relatively detailed article.
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.
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).
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?
Plus, LLMs have really made upgrading a codebase for a compiler or dependency update into a trivial chore, at least for the most part.
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.
Unless you just have an axe to grind?
really says a lot that someone can say this apparently absolutely seriously
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.
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.
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".
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.