impl Trait fails to resolve when returning !

7b87138
Opened by Cobrand at 2024-12-21 05:10:34

I couldn't think of a better title.

This code doesn't compile on nightly :

#![feature(conservative_impl_trait)]
use std::ops::Add ;

fn test() -> impl Add<u32> {
    unimplemented!()
}

fn main() {}
rustc 1.13.0-nightly (378195665 2016-09-08)
error[E0277]: the trait bound `(): std::ops::Add<u32>` is not satisfied

while this does :

#![feature(conservative_impl_trait)]
use std::ops::Add ;

fn test() -> impl Add<u32> {
    if true {
        unimplemented!()
    } else {
        0
    }
}

fn main() {}
  1. Currently, ! does not implement Add<u32>. That's the underlying issue here. In order to fix this in general, IMO we should generate automatic ! impls of all traits containing only non-static methods.

    Taylor Cramer at 2018-01-05 21:10:07

  2. Considering that ! can coerce to any other type, it seems strange that it can't coerce to any impl Trait (even if it doesn't explicitly implement that trait). It would seem like an inconsistency to only implement traits that contain non-static methods. As long as an object exists that implements that trait, ! could trivially coerce to that type, so you could do it by casting beforehand. I think this should "just work" (assuming there aren't any compatibility issues with doing it).

    varkor at 2018-01-12 11:22:04

  3. The issue is which type you coerce to. There can be multiple, and the choice can be observed if there are static methods: https://play.rust-lang.org/?gist=f8679a05f75f3db559870ad2389713bb&version=nightly

    Hanna Kruppe at 2018-01-12 11:35:27

  4. @rkruppe: Ah, I see; that's more awkward. The "non-static implementations only" causes issues too, though, because you'd always be able to get around it with this unintuitive if true { ... } else { valid_value } construction. Tricky.

    varkor at 2018-01-12 11:41:20

  5. if true { <diverges> } else { <something of type T> } unambiguously has type T. So there's no ambiguity. It's just a very ugly workaround.

    Hanna Kruppe at 2018-01-12 12:58:28

  6. Yeah, it's like explicitly casting the ! to any other type (that implements the trait) in the return position — it just feels very ugly, like you say (though casting is possibly somewhat nicer — and in the worst case, we could probably lint this case).

    varkor at 2018-01-12 13:24:23

  7. cc https://github.com/rust-lang/rust/issues/34511#issuecomment-322340401

    kennytm at 2018-01-12 14:31:21

  8. If () implements the return trait, compilation succeeds.

    use std::fmt::Debug;
    
    fn main() {
        println!("{:?}", frob());
    }
    
    fn frob() -> impl Debug {
        unimplemented!()
    }
    

    Andrew Dirksen at 2019-03-28 18:08:27

  9. I recently got bitten by this while working with Iterator, which led me to this issue.

    The impl Trait RFC names impl Iterator as a major use case for that syntax, since it's a place where exact types get very complex, but code frequently only cares about the impl Iterator part. With current limitations in place, one has to choose between two unappealing options:

    • don't use todo!() or unimplemented!(), which are awesome for prototyping, or
    • specify the exact type, and don't use impl Iterator, which the impl Trait RFC eloquently argues is very unergonomic.

    I understand that returning ! in general for impl Trait is difficult to do — but given that impl Iterator is a primary use case of impl Trait, would it make sense to special-case support for returning ! in impl Iterator functions?

    As one possible approach, ! could perhaps be made to implement Iterator and simply have a next() that always returns None. A code example is below, though it currently doesn't compile because neither ! nor Iterator are defined in the current crate:

    #![feature(never_type)]
    
    impl Iterator for ! {
        type Item = u32;
    
        fn next(&mut self) -> Option<u32> {
            None
        }
    }
    
    fn my_func() -> impl Iterator<Item = u32> {
        todo!()
    }
    

    https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=c754a0e11b6bc3931d124e41681b2562

    I have not figured out a way to implement Iterator<Item = T> for any T, since ! is not generic itself and I don't control its definition so I can't add a PhantomData<&T>. But I'm fairly new to Rust, so perhaps more experienced folks might have some ideas.

    Thanks for all your hard work making Rust awesome!

    Predrag Gruevski at 2021-03-02 23:54:11

  10. How about this? When ! is coerced to impl Trait, the compiler creates an impl Trait for ! with the following rules:

    • All associated functions panic, along the lines of panic!("<! as FooTrait>::baz()")
    • All associated constants panic, replaced with a panic!("<! as FooTrait>::BAR"). It could either set the value of the constant as panic!(), possible now with panic in const fn, or there could be special glue that replaces all access in non-const contexts with a panic to still allow it to compile. I prefer the former, as it's more consistent with the rest of the language. Fortunately, you only run into the compile-time panic if you actually access the associated constant.
    • All associated types are !, following the same impl Trait rules in case of trait bounds
    • All necessary supertraits are implemented with the same rules

    Alyssa Haroldsen at 2022-01-28 19:41:54

  11. When ! is coerced to impl Trait, the compiler creates an impl Trait for !

    As stated, that would violate trait coherence, since the crate defining Trait should be allowed to impl Trait for !. That could be worked around: for purposes of the -> impl Trait type resolution, the function could pretend to return an anonymous implementing type that isn't actually ! itself.

    A more serious surprise would be action-at-a-distance panics: if the return type gets unified with some other type variable, then the impl's associated functions could be called, and panic, even if the !-returning function is not ever called at all. The following program is a toy example which panics on current nightly, and if the above were implemented, would panic without the impl Foo for ! — without any explicit panics or triggering any library function panics:

    #![feature(never_type)]
    trait Foo {
       fn recommended_quantity() -> usize;
    }
    
    fn do_thing<T: Foo>(initialize: bool, f: fn() -> T) -> Vec<T>  {
        let mut v = Vec::with_capacity(T::recommended_quantity());
        if initialize {
            v.resize_with(T::recommended_quantity(), f);
        }
        v
    }
    
    impl Foo for ! {
        fn recommended_quantity() -> usize {
            panic!("hypothetically, this implementation is implicit");
        }
    }
    
    fn main() {
        fn never() -> ! { loop {} }
        do_thing(false, never);
    }
    

    Notice in particular that the -> ! function isn't even panicking; it's looping infinitely, which could be an entirely sensible main-loop function, perhaps (though not one you'd likely want to pass to this do_thing() example). The proposed implicit impl converts it from a compilation failure fixable by adding an impl, to a program which panics before it even calls the diverging function.

    One way to describe this problem would be that it violates the suggested principle of implementing for !:

    When writing your own traits, ! should have an impl whenever there is an obvious impl which doesn’t panic!.

    This would implicitly make impls which observably do panic.

    Kevin Reid at 2022-03-11 05:39:27