I think I found a bug where this error is thrown in a place where it shouldn't. It doesn't matter whether I panic directly or via these meaningful macros, it all leads to the same:
pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
where
R: RangeBounds<usize>,
{
unimplemented!("Drain is not implemented.");
}
Error:
error[E0277]: `()` is not an iterator
--> src/main.rs:5:45
|
5 | pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `()` is not an iterator
|
= help: the trait `Iterator` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Why does the impl Trait functionality still look for the real return value instead of silencing the parser due to an unreachable code? These macros, as well as panics, work all the time correctly in all the other places, but it seems when it comes to impl traits, it ends like that.
If one needs a reason for me having this method - it is because I am rewriting it and have dozens of tests on it already, which I'd prefer to have to fail rather than commenting out and then forgetting to uncomment back. Like simple TDD.
Note: it also fails as a stand-alone function, not just as a method.
Because there still needs to be a type that implements Trait to be chosen. That's just what impl Trait means: the return type is some type that implements Trait. It doesn't matter whether the function ever returns.
How to make it compile anyways? Give it a type hint for some choice of Iterator in an untaken branch. Something like
pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
where
R: RangeBounds<usize>,
{
if false { return std::iter::empty() }
unimplemented!("Drain is not implemented.");
}
I don't think this is correct. Regardless of the type returned, if the code is unreachable, it should never be checked for. It already works exactly this way, with no exceptions besides this one. This leads to inconsistent behaviour, which one does not expect, knowing that everywhere else, it works another way.
I understand that the compiler needs to decide on a place where and how it allocates the return value and what it does with it, but in this case, since the code is unreachable, it can just assume "returning an empty iterator" exactly the same way as you did, or not doing anything at all, since the invocation will lead to an immediate unwinding process. I think this can be improved, even in this case. Treating it like a ! instead, then, perhaps, or an empty iterator, or anything else which is better/easy/suitable, but there should be no inconsistencies; everything should be predictable and consistent. Not to mention that the error message could have mentioned the real problem instead of simply falling through to the return value which is unreachable.
Thank you for giving me a way to return an empty iterator.
Even if drain never returns, it still has a return type. Some traits do things without the need for a value present. You can start with the function type of drain, look at its return type, and have some generic function use that trait.
Now, demo() prints 42. Also demo never calls foo_function. In fact foo_function never returns a value. Yet the if false branch specifying the return type S with constant C = 42 was crucial. The compiler rightfully errors if that’s missing:
error[E0277]: the trait bound `(): Foo` is not satisfied
--> src/main.rs:5:22
|
5 | fn foo_function() -> impl Foo {
| ^^^^^^^^ the trait `Foo` is not implemented for `()`
|
Now, Foo above is different from Iterator, since Iterator really doesn’t do much without having an instance of it. (It’s even object safe.) Perhaps special-case rules for such traits could be feasible, and an impl Trait return type could be allowed to be left missing in such cases. But also, that kind of special-casing would be somewhat less consistent; the impl Trait return feature doesn’t inherently have anything to do with – say – object safety [1]
Edit: Though it might change in the future, anyways. There are people interested in making ! automatically implement certain traits. Presumably, a function that never returns could then simply return ! and that’d be compatible with impl Trait return types for any such trait that ! implements. Anyways, this is in the category of possible (nontrivial) future language features, and the lack of such a feature does at least in my opinion clearly not warrant calling it a “bug”
Now, who would write a code like this and who would even imagine using (and/or writing) a function this way to do this thing? I understand that this is an artificial scenario which proves your point, I agree, but it doesn't mean this code is even correct engineering-wise since the actual implementation is statically figured to be struct S due to the parser and early analysis (which is even partially wrong, since the path taken at compile-time to analyse the return value is never actually taken at runtime), yet you can never rely on it being exactly this type, not to mention that, in my opinion, it should have never been analysed this way, since the if false branch will either be optimised out or simply never executed. I have seen lots of different code in C++, but even this is far from the weirdest SFINAE techniques and other "pretty things" I have seen.
As an example to prove your point - this one is great; thank you, I fully understand what you meant. But it doesn't prove the behaviour should be this way all the time, especially if a function never returns, since it 1) never returns, 2) the returning code is unreachable, 3) the logic based on it would be wrongly assuming the returned value 4) violates the consistency of the reachable and unreachable code, since in all the other places except impl Trait it works one way, but here it works differently. In the end, 5) regardless of the return type and possible return value, if the code leads to a final branch as in here (a panic), no one cares about the value/type/anything returned. The compile-time type deduction here, based on the if false hack, just shows that it is possible, but not that this is a good practice or correct with regard to the other things.
I sincerely thank you for your elaborate answer. In the end, I do not want to argue, I just want things to become a little better
Thinking of ways to improve upon the if false “hack”, I was wondering whether something like this would be nicer
pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
where
R: RangeBounds<usize>,
{
unimplemented!("Drain is not implemented.") as std::iter::Empty<_>
}
it works! But also, it gives off an “unreachable expression” warning, unfortunately, even though the as std::iter::Empty<_> part – despite never being executed – turns out crucial for providing the necessary type information here.[1]
I guess, this following alternative approach (which works, too) is quite similar as well (also produces “unreachable expression”):
pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
where
R: RangeBounds<usize>,
{
unimplemented!("Drain is not implemented.");
std::iter::empty()
}
Anyways, suppressing the warning for now of course is an option, too
pub fn drain<R>(&mut self, range: R) -> impl std::iter::Iterator<Item = u64>
where
R: RangeBounds<usize>,
{
#![allow(unreachable_code)]
unimplemented!("Drain is not implemented.") as std::iter::Empty<_>
}
The confusing thing from the DevX point of view is that this:
fn foo() -> i32 {
todo!()
}
of course works perfectly well without a dummy return value, with the ! pseudo-type coercing to any concrete type without problem. But refactor that to
fn foo() -> impl Display {
todo!()
}
and it suddenly stops working. I sort of understand why it is so (due to the () fallback hack in the absence of ! as a proper bottom type), but it's weird and arbitrary from the viewpoint of any programmer who's not familiar with Rust type system peculiarities.
There's some recent discussion on ! trait implementations in this IRLO thread, and similar conversations have happened before.
If () doesn't implement the trait (or if ! doesn't, with never type fallback), another reason it may not work is that the compiler probably wants to be able to infer the auto-traits of the opaque type as opposed to making them up. Sort of like how you must use a type parameter in a struct so that it can determine auto-traits and variance.
I think that it is a good guiding principle to first and foremost build a language in a way that is self-consistent and rule-based in the same way mathematical systems feel like. It creates a solid foundation for building more stuff on top of it that pays out in the long run by preventing edge cases from piling up in exponential permutations in ways that weren't initially anticipated.
That said, in this specific case, I still think it there might be a good way for the compiler to solve it. Perhaps, say, to have types which return impl Trait and which never return normally be able to auto-generate an anonymous uninhabited implementation type, so long as there aren't any associated types or constants that aren't specified by the function's signature, in which case a compile error could specifically point out that that's the case.
I think so too. It shouldn’t necessarily overload ! itself by adding all kinds of implicit trait implementations to it (and it can’t anyways, as soon as associated types are involved), but an anonymous uninhabited type sounds decent.
Second, the current behavior of the never type is a compromise between adherence to the definition of a divergent type and backwards compatibility. Folks working on the type system have been aware of significant regressions that would be introduced without this current behavior (called "fallback", viz., the never_type_fallback nightly feature mentioned in several places) since 2019.
EDIT: Ah, I understand now that @quinedot alluded to these already.
The Rust compiler could, of course, set the return type to some pre-determined type like the empty iterator in case where it detects that all code branches cause a panic. This may feel "natural" or "logical", but it has many problems.
First, it might be non-trivial to check if all branches result in a panic. Second, this would work for some traits and not for others, which would make it inconsistent and confusing. And finally, this kind of "magic" behaviour where a default thing is selected "behind the user's back" is exactly the kind of thing that Rust tries to minimize, and for a good reason.
Being more explicit is more typing, but it makes the code clearer to read and it is probably better for the compiler anyway. That's why you have to re-declare function signatures in trait implementations, even when they have to be the same as in the trait definition, so you could (in theory) omit them.