5 ms·
Stabilizing Rust's Never Type
- epolanski 21d agoThe 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 } }
- xg15 21d ago> 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 treat it as if it implemented every trait too?
- tux3 21d agoThe 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.
- dlubarov 21d agoMoreover, Rust traits' associated constants/types get in the way of having a proper bottom type. What would <! as Iterator>::Item be? (In Scala I think it just doesn't compile?)
- SkiFire13 21d agoNot only that, if you have `trait Foo: Iterator<Item = u8>` and `trait Bar: Iterator<Item = i32>` how could `!` implement both of them? It would simply make the language incoherent.
- 10000truths 21d ago> What would <! as Iterator>::Item be? 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!()`.
- xg15 21d agoAh, that makes sense. Rust noob here, so I wasn't aware traits can act on types directly without any instance of the type. Thanks for the info!
- Sharlin 21d agoYep, they can have static methods, as it were (in Rust lingo called "associated functions"; "methods" in Rust always take a `self` receiver). Traits can also have associated types and associated constants, which (naturally) also relate to the type, not any particular instance.
- deleted 21d ago[deleted]
- weinzierl 21d agoRelevant talk by Waffle at RustWeek earlier this year: "When is never?" https://youtube.com/watch?v=3jM4cnEVrLc https://youtube.com/watch?v=3jM4cnEVrLc
- munchler 21d agoAs a fan of the Curry-Howard correspondence, I approve of this decision.
- SabrinaJewson 21d agoYou’re going to hate when you learn that the never type is inhabited
- kibwen 21d agoI'm unclear what this is implying, but in Rust the never type is uninhabited. https://doc.rust-lang.org/reference/items/enumerations.html#r-items.enum.empty.uninhabited https://doc.rust-lang.org/reference/items/enumerations.html#...
- ratmice 21d agoPerhaps it refers to PhantomData<!> but I don't know
- SabrinaJewson 21d agoIn 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.
- LatticeAnimal 21d agoIs 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)
- kibwen 21d ago> Is it obvious to rust developers that "!" would be the never type? 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-lang.org/doc/tutorial/func.html 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).
- ketzu 21d ago> Prior to this change most Rust developers would never have cause to ever use `!` for any reason. 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.
- kccqzy 21d agoThe lesson here is that implicit conversions are bad. The never type itself having implicit conversions to other types is bad enough (even though such coercions are logically valid: “ex falso quodlibet” they should be explicit), but having a fallback type when type inference doesn’t have enough information to produce a type is even worse. Rust is famous for not even having implicit numeric coercions (say from i8 to i32) but it seems like a shortsighted decision to allow implicit coercions here.
- jadenPete 21d agoWhy 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.
- kccqzy 21d agoNo I’m not talking about runtime conversions. I’m talking about conversions that happen at type inference time. 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.
- SabrinaJewson 21d agoDo you have an example of a piece of code that behaves in a surprising way because of this rule?
- cipherjim 21d agoMy 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.
- tialaramex 21d agoAnd 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.
- tyre 21d agoDoes that mean you can assign the result of the function without explicitly unwrapping? Or that the compiler removes the call to unwrap? Or neither?
- bonzini 21d agoYou 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();
- pascahousut 21d agoMy 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.
- Georgelemental 21d ago> 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 downside of its lack of compiler support is losing out on the coercions. (The article is excellent otherwise)
- tialaramex 21d agoWhich 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.
- nextaccountic 21d agoNote, 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
- kevinbaiv 21d ago[dead]
- HNBeLike 21d agoSounds 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.
- salsa_catsup 21d agoIs this so central, that it justifies the use of a single ascii char? Instead of, say, `Never`?
- db48x 21d agoI 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.
- deleted 21d ago[deleted]
- simonask 21d agoIt'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).
- pie_flavor 21d agoIt 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?
- ddosmax556 21d agoIt 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.
- srdjanr 21d agoI 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
- geocar 21d agoI 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?
- nixpulvis 21d agoYou could keep using the old compiler, no?
- Ygg2 21d agoYes, but new compiler can't use old ecosystem crates. Hence the issue. Rust's edition system gives you the best of both worlds, with caveats. Some changes will be impossible.
- psd1 21d agoEverything 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?
- pseudocomposer 21d agoI 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.
- Analemma_ 21d agoIt’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.
- kelvo_ran 21d ago[dead]