`()` is not an iterator bug in "impl Trait"s

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.

1 Like

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.");
}

should do.

1 Like

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.

1 Like

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.

For example...

trait Foo {
    const C: i32;
}

fn foo_function() -> impl Foo {
    if false {
        struct S;
        impl Foo for S {
            const C: i32 = 42;
        }
        return S;
    }
    unimplemented!("todo!");
}

trait HasFooReturnType {
    type ReturnType: Foo;
}

impl<F: Fn() -> T, T: Foo> HasFooReturnType for F {
    type ReturnType = T;
}

fn expect_fn_returning_foo<F: HasFooReturnType>(_: F) -> i32 {
    F::ReturnType::C
}

fn demo() {
    println!("{}", expect_fn_returning_foo(foo_function))
}

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:

fn foo_function() -> impl Foo {
    unimplemented!("todo!");
}
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 :slightly_smiling_face:[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” :wink:


  1. and any alternative characterization of “traits of this and that form support this” have the potential to introduce new conditions that limit semver-compatible extension of traits with new (defaulted) methods ↩︎

7 Likes

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 :slight_smile:

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<_>
}

  1. This is something I personally would call a “bug” :wink: ↩︎

4 Likes

I guess the second example here is the best so far, except, of course, the warning that rightfully says the code is unreachable.

It's confusing that the compiler is looking for impl Iterator for () rather than impl Iterator for !. This seems like undocumented behavior.

Also logically ! could easily implement Iterator. It already implements some random other traits, such as Not.

3 Likes

That's nontrivial because of the associated type; what should be the value of <! as Iterator>::Item?

Logically, it should also be ! as nothing will ever be yielded, but that position can't coerce to other types at the moment.

7 Likes

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.

3 Likes

Perhaps more generally, a function that returns impl Iterator does need to return some sort of iterator, so you can't do:

fn unimplemented_iter() -> impl Iterator<Item = u64> {
    todo!()
}

because the trait Iterator is not implemented for ()

But what you can do is define a dummy iterator that always fails, like:

fn unimplemented_iter() -> impl Iterator<Item = u64> {
    std::iter::from_fn(|| todo!())
}
3 Likes

See feature(never_type_fallback) or the related issues.

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.

1 Like

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.

7 Likes

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.

4 Likes

Most of these comments seem to be lacking public historical context. Perhaps this is some indirect feedback on the accessibility of this information?

First, there is an active issue in Rust upstream tracking this exact problem: `!` cannot be used as `impl Trait` when `!` implements `Trait` · Issue #105284 · rust-lang/rust · GitHub

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. :saluting_face:

3 Likes

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.

I think there's a problem with your approach where the function itself succeeds and the iterator fails only when it's iterated on.

This means that if the iterator is only used later it will only error later, or if doesn't get iterated at all the code will not error.

Also, even if it does get iterated immediately, the backtraces will be a bit different and debugging it will be less convenient.

Maybe this trick can work with some kind of iterator that eagerly evaluates a value?

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.