Today I saw a StackOverflow question that describes a problem that currently has no satisfying solution.
The problem, in short, is this: you cannot put e.g. panic!() or todo!() in a function returning impl Trait, as the compiler cannot infer the type, so it deduces !, but it does not implement the trait.
The never type cannot implement any trait; even with a magical language feature, it cannot implement e.g. Default. However, this is not a real problem here: we don't use ! as a type, and there is no risk of calling any method of it. The code is just unreachable.
The treatment of never-returning as a type is neat, but this makes me think perhaps it's a bad idea: maybe we should have a #[no_return] attribute for functions (or something alike), that will truly work for any type, not just coerce for any type?
Edit: I copy my later insight here:
However, thinking about it more, we don't need to separate !. We can just declare that if you have a value of type ! (with some rules about references because of unsafe code), the code is truly unreachable, and type checking is not performed, or not performed to some extent.
We can declare that the type of the function when it's used in a callback is fn ... -> !, but when it's called a stronger control flow check applies that will treat it as unreachable. That will be harder to implement though, as currently calling a function is a separate path expression and call expression.
However, thinking about it more, we don't need to separate !. We can just declare that if you have a value of type ! (with some rules about references because of unsafe code), the code is truly unreachable, and type checking is not performed, or not performed to some extent.
then wouldn't this Just Work, in the sense that (a) it's type consistent, and (b) it has the only runtime behavior it could have that's consistent with all the type signatures?
Although impl Default for ! wouldn't be unsound, it wouldn't comply with the specification of Default, and so it would be an incorrect implementation and might easily lead to programs having incorrect (but defined) behaviour.
Yes, but. Now the code can be compiling and running when it should be failing to compile with some "unconditional panic" warning/error. What we probably want is:
I can think of several scenarios (mostly involving todo!()) where I would want code like this to compile successfully. (It would be fine for it to do nothing useful at runtime.)
There's simply no single valid type for panic!() because no type can implement the same trait twice with different associated types as that would be unsound.
Default values are often some kind of initial value, identity value, or anything else that may make sense as a default.
Panicking does not comply with this specification because it doesn't return a value at all – and a trait implementation only complies with the specification of a trait if it complies with the specification of all its methods.
I used to be annoyed at this too, but I have come to realise that all the ways it could be "fixed" would be worse, and also wouldn't actually work.
An idea that struck me as I was reading this thread however, what if it is allowed as long as the function (or the type of it) is never used. This would allow writing todo!() while still working on a new leaf function, but not if it gets used anywhere. I don't know if that is feasible though, as it is a kind of whole program analysis. You could perhaps make it an error at the call site, but this seems quite complex for little gain.
One workaround you can do is to add a dummy return on another branch in the function. E.g. return T::default() or an empty iterator, etc.
We don't need a single valid type here, though. We need ! as Iterator<Item = u32> and ! as Iterator<Item = String>, which could be two different uninhabited types. (Consider "as" here as a type refinement operator, like u32 as 1..= in the various proposals for restricted range integers.)
Sure it does. A function that is declared to return an uninhabited type, complies with that type signature if and only if it does not return. That's what returning an uninhabited type means.
I still disagree. Panicking is not returning a value at all.
If a function is declared as, say, fn foo() -> u32, then its possible options are either returning a u32, entering an infinite loop, or panicking/aborting (which does not return). If it is defined as returning rather than panicking, panicking would violate the specification.
If the function is declared as fn foo() -> !, returning is no longer an option – it must enter an infinite loop or panic/abort. If it is defined as returning rather than panicking, then panicking still violates the specification (which is no longer possible to comply with).
I think you're leaning way too hard on informal language. Nearly all Rust stdlib functions can panic. If Default impls were required never to panic, I would expect it to say so explicitly in the doc for Default.
Rust stdlib functions nearly always document the circumstances under which they can panic, and are assumed to not panic otherwise.
If they allowed undocumented panics for non-obvious reasons, this would make it very difficult to write a correct Rust program, because you would have to guard against the possibility that the stdlib would choose to panic rather than do what the code requested it to do. Perhaps the behaviour of panicking on <! as Default>::default() is "obvious" in a sense – but it becomes less obvious when it's buried deep in code with a generic T: Default bound that is assuming that default() is behaving in a way that matches its specification, and thus doesn't document that it might panic if default() panics (because this isn't an expected outcome). This is expecially true if the function in question doesn't return a T.
I have never heard anyone make this claim before and I don't think it's backed up by the text of the stdlib documentation.
you would have to guard against the possibility that the stdlib would choose to panic [at any point]
Yes. Yes, you do have to do that. I've been being paranoid about that ever since I learned the language!
I would really like it if the situation were more like you seem to think it is; in particular, if all functions that return a Result were guaranteed not to panic except when the concrete Result<T, E> is uninhabited, it would help a lot.
It would also help a lot, and would probably be an easier incremental step from where we are today, if there were a blanket guarantee that, unless explicitly documented otherwise, all stdlib functions only panic in one of these three circumstances:
memory allocation fails
a documented caller-must precondition is violated
their return type is uninhabited
Regardless, any time you see an uninhabited type in return position, you know that function will never return, so I still don't buy your objection. Of course the correct impl of Default for ! is an unconditional panic. There is nothing else it could be.
It could be “no such implementation”, which I think would be significantly better. In particular, I would expect that the presence of ! would cause other compound types to not impl Default. I would be unpleasantly surprised if
I don't think this is true. There are many functions that can panic if allocation fails, and only a handful of them document that. And that is the most important panic condition that can be feasibly hit by arguably bug free programs.