73 comments

[ 0.18 ms ] story [ 4.5 ms ] thread
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.

(comment deleted)
As a fan of the Curry-Howard correspondence, I approve of this decision.
You’re going to hate when you learn that the never type is inhabited
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)
Yeah it really baffles me why a symbol like `!` was spent on this, which could be more useful for more a more commonly used feature.

I just checked, my main side project only has less than 10 things that return never. `-> Never` reads even better, imo.

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 = !;`.

Yeah I think just calling it 'Never' is much clearer, this isn't something that needs a dedicated single character, and ! is less readable imo
Agree. This is also what Swift uses.
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.

[0]: Example: std::process::exit returns `!` https://doc.rust-lang.org/1.0.0/std/process/fn.exit.html

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.
[delayed]
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.

[delayed]
Do you have an example of a piece of code that behaves in a surprising way because of this rule?
[delayed]
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.
> so they chose the unit type as a relatively benign alternative in those contexts

This is what the article defines as fallback. So the issue has everything to do with fallback.

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.

Java doesn’t permit that. You must specify covariant or contravariant type parameters with <? extends T> or <? super T>.
You are misled for two reasons.

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.
Does that mean you can assign the result of the function without explicitly unwrapping? Or that the compiler removes the call to unwrap? Or neither?
You need to unwrap but the compiler removes the call. If instead you know that the callee is infallible you can also do

    let Ok(x) = call_that_cant_fail();
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

    struct MyUnitType {}
... and its size is zero.
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
Just like you would expect from algebra. The empty sum is 0, the empty product is 1.
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.
tialaramex got it wrong even at the type theory level, a size of 0 is the wrong focus, the number of elements, 1, should have been the focus.
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.
> remember to kill all pedophile murderers like...

What's this bullshit?

> (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.

Do you have an example? I'm struggling to think of such a case where ! is ambiguous between a end of macro call and the Never type.
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).
Sounds serious. Armageddon serious.

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.

Is this so central, that it justifies the use of a single ascii char? Instead of, say, `Never`?
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 has been in the language for fourteen years. What would be the purpose of changing it? And what else would ! in the type position signify?
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
I don’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?

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.

The answer is to keep the compiler and the OS image it is running on alongside the source in version control. (Docker alone won’t save you.)

Unless you just have an axe to grind?

> The answer is to keep the compiler and the OS image it is running on alongside the source in version control.

really says a lot that someone can say this apparently absolutely seriously

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.

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.

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".

Yeah this is good. I feel like a language which never fixes a mistake ends up stuck. JS by it's nature can't fix things like this and it sucks.

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.