The longer error message would have been emitted regardless of the warning:
warning: trait objects without an explicit `dyn` are deprecated
--> file.rs:3:13
|
3 | fn foo() -> Trait {
| ^^^^^ help: use `dyn`: `dyn Trait`
|
= note: `#[warn(bare_trait_objects)]` on by default
error[E0746]: return type cannot have an unboxed trait object
--> file.rs:3:13
|
3 | fn foo() -> Trait {
| ^^^^^ doesn't have a size known at compile-time
|
help: use some type `T` that is `T: Sized` as the return type if all return paths have the same type
|
3 | fn foo() -> T {
| ^
help: use `impl Trait` as the return type if all return paths have the same type but you want to expose only the trait in the signature
|
3 | fn foo() -> impl Trait {
| ^^^^^^^^^^
help: use a boxed trait object if all return paths implement trait `Trait`
|
3 | fn foo() -> Box<dyn Trait> {
| ^^^^^^^^^^^^^^
Edit: and for some more realistic cases where the compiler can actually look at what you wrote, instead of just giving up because you used `todo!()`:
warning: trait objects without an explicit `dyn` are deprecated
--> file.rs:7:20
|
7 | fn foo(x: bool) -> Trait {
| ^^^^^ help: use `dyn`: `dyn Trait`
|
= note: `#[warn(bare_trait_objects)]` on by default
error[E0308]: `if` and `else` have incompatible types
--> file.rs:11:9
|
8 | / if x {
9 | | S
| | - expected because of this
10 | | } else {
11 | | D
| | ^ expected struct `S`, found struct `D`
12 | | }
| |_____- `if` and `else` have incompatible types
error[E0746]: return type cannot have an unboxed trait object
--> file.rs:7:20
|
7 | fn foo(x: bool) -> Trait {
| ^^^^^ doesn't have a size known at compile-time
|
= note: for information on trait objects, see <https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types>
= note: if all the returned values were of the same type you could use `impl Trait` as the return type
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
= note: you can create a new `enum` with a variant for each returned type
help: return a boxed trait object instead
|
7 | fn foo(x: bool) -> Box<dyn Trait> {
8 | if x {
9 | Box::new(S)
10| } else {
11| Box::new(D)
|
and
warning: trait objects without an explicit `dyn` are deprecated
--> file.rs:7:20
|
7 | fn foo(x: bool) -> Trait {
| ^^^^^ help: use `dyn`: `dyn Trait`
|
= note: `#[warn(bare_trait_objects)]` on by default
error[E0746]: return type cannot have an unboxed trait object
--> file.rs:7:20
|
7 | fn foo(x: bool) -> Trait {
| ^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Trait` as the return type, as all return paths are of type `S`, which implements `Trait`
|
7 | fn foo(x: bool) -> impl Trait {
| ^^^^^^^^^^
error[E0308]: `if` and `else` have incompatible types
before.
I was trying to write a simple little program that would output data and optionally reversed (like sort -r). Nothing I could do with iterators would work because it kept trying to tell me that a reverse iterator was not compatible with an iterator. It would work if I hardcoded it, but any code like
output = if (reverse) { result.rev() } else { result };
would fail to compile with a completely nonsense error message. I think I eventually got it to work by collecting it into a vector first and reversing that. Haven't really touched rust since. Honestly the compiler might as well just tell me to go fuck myself.
> would fail to compile with a completely nonsense error message.
How long ago was this? We spend a lot of time trying to make the error messages easy to follow and informative. If it wasn't that long ago, I would love to see the exact cases you had trouble with in order to improve them.
> I think I eventually got it to work by collecting it into a vector first and reversing that.
Collecting and reversing is indeed what I would do if I couldn't procure a reversible iterator (DoubleEndedIterator), but if you have one, you can write the code you wanted by spending some cost on a fat pointer by boxing the alternatives[1].
> Haven't really touched rust since. Honestly the compiler might as well just tell me to go fuck myself.
I'm sad to hear that, both because you bounced off (which is understandable) and because that experience goes counter to what we aim for. We dedicate a lot of effort on making errors not only readable, pedagogic and actionable (with varying levels of success). We really don't want the compiler to come across as antagonistic or patronizing.
Edit: For what is worth, type mismatch errors do try to give you appropriate suggestions, but in the case of two types that both implement the same trait (like the one you mention), the compiler does not look for traits that are implemented for both:
error[E0308]: `if` and `else` have incompatible types
--> file.rs:10:9
|
7 | let x = if true {
| _____________-
8 | | A
| | - expected because of this
9 | | } else {
10 | | B
| | ^ expected struct `A`, found struct `B`
11 | | };
| |_____- `if` and `else` have incompatible types
This could be done, but that has the potential to give you a lot of output for all possible traits that could satisfy this code, and in general we would be sending you in the wrong direction. When we can't be 90% sure that the suggestion is not harmful, we just "say nothing", like in this case. On a real case of the example above, you'd be more likely to want to create a new enum.
Per the sibling conversations: instead of having the compiler tell users about traits that are implemented by both arm types, maybe it would be more productive to tell users how the issue arises from static dispatch considerations?
Maybe if there's an attempt to invoke a method on the result later, like in this case, the compiler could point to it in a "note" and say "Would not be able to determine statically which `impl` of this method to invoke", or something.
Users with experience in languages like Java and Python will have a reasonable expectation that code like this should work, because "they both implement iteration" [0]. It's definitely not obvious that dynamic dispatch is why that can work, and how Rust's static dispatching default impacts idioms like this.
It's singularly frustrating to try to express yourself in a language where familiar idioms just don't carry anymore -- as anyone who's gone from Haskell to Java can attest. I think it's valuable to recognize the idiom and gently teach users why Rust differs.
The problem for the `if/else` case is that we need to identify the trait that both arms should be coerced to, and that set could potentially be huge, and any suggestion should skip things like std::fmt::Display. That's why the suggestion I showed earlier only works on tail expressions, we can get the type from the return type and work from there, and even account for someone writing `-> Trait` and suggest all the appropriate changes.
I just filed https://github.com/rust-lang/rust/issues/84346 to account for some cases brought up and I completely agree with your last sentence. It is something that others and I have been doing piecemeal for a while now and would encourage you (and anyone else reading this) to file tickets for things like these at https://github.com/rust-lang/rust/issues/
In some of the cases in this discussion, isn't the problem the compiler has to solve a little bit simpler? From (a) the indeterminate type of the result and (b) the invocation of a method on that value within the same scope, we should be able to infer the trait that the user is relying on. (Assuming the trait itself is in scope, which, if it isn't, is already an error that hunts for appropriate traits to suggest, I think?)
In some other cases we've discussed here, the actual trait we want is named in the return type, which also fills the role of (b) above. I think this is the case you outlined.
I guess my point is, it seems like we already have enough local information to avoid doing a type-driven trait search. In one case, we have a method and two non-unifiable types, and in the other, we have a trait and two non-unifiable types. I can see how the more general case of "just two non-unifiable types" would be hard, but I'm not sure we have to solve that to cover a meaningful chunk of the problem space.
I would almost say iterators could/should be treated as a special case here (in terms of informing the compiler message). It's extremely unintuitive that adding a transform to an iterative gives you a totally different concrete type. I understand why this is, but most people won't, and it's one of the most prominent cases of this general problem in my experience.
That snippet gives a warning: "trait objects without an explicit `dyn` are deprecated". Adding the `dyn` in the right place (`&mut dyn Iterator<Item=i32>`) makes it a little more clear that you're still paying the costs of a fat pointer (half for the trait object pointer, half for the instance pointer), even if the instance is indeed stack-allocated and not heap-allocated ("Box").
If you're returning the `dyn Iterator` from this function, you'd likely need to Box it anyway, since it will go out of scope on return. (Of course, you inlined the function to account for this ;) )
None of which is to say you're wrong; only that different solutions will be appropriate for different cases. "Box" will probably work more consistently in more cases, but it's definitely valuable to have the stack-allocated approach when it's applicable.
= note: expected type `Rev<std::vec::IntoIter<_>>`
found struct `std::vec::IntoIter<_>`
it could not be more unhelpful. I know those things are different, however the following "for i in output" works for both of those things individually, so why does it matter that the types are different since they both implement iteration?
I think the key idea here is that Rust consistently uses static dispatch by default. When you invoke some method defined by a trait, it needs to look up at compile-time which actual implementation it should dispatch to. Since the if-else expression isn't returning the same type, it doesn't matter if they both implement Iterator -- Rust still doesn't know which actual type will be produced, so it won't be able to tell which method implementations should actually be dispatched to.
Dynamic dispatch, which solves the issue you faced, needs to be explicitly opted into using the `dyn Trait` syntax, since it introduces a hidden indirection through a fat pointer.
This is definitely a difference from languages like Java or Python, where dynamic dispatch is the default (and sometimes there's no way to explicitly use static dispatch). On the other hand, languages like C and C++ also use static dispatch by default, with mechanisms like function pointers or `virtual` to provide dynamic dispatch as needed.
You would very likely have faced a similar problem in C++, had you used `auto x = (..) ? ... : ...`; (If you used `T x = ...; if (...) { x = ... } ...`, you'd have been faced immediately with the issue of "what type should T be" anyway, I think.)
I ran into that issue trying to port something else to rust, just didn't realize it was this same issue. In go you just define an interface and then make a slice of that interface and put things in it. In rust I ended up having to do
Vec<Box<dyn Checker>>
I think initially I tried just doing a
Vec<Checker>
and when that failed I ended up putting something like "How do I make a vec of an impl in rust" and found a code sample.
That's where the compiler just saying "The types don't match" is not very helpful.
I think the fundamental issue is that most languages these days use dynamic-dispatch invisibly and by default. Rust seeks to empower users to be more efficient by default (which is good), but sometimes, especially on this iterators case, it creates deeply confusing/frustrating barriers that have to be explicitly stepped around.
error[E0277]: the size for values of type `dyn std::fmt::Display` cannot be known at compilation time
--> src/main.rs:5:12
|
5 | let y: Vec<dyn Display> = x.into_iter().collect();
| ^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `dyn std::fmt::Display`
error[E0277]: a value of type `Vec<dyn std::fmt::Display>` cannot be built from an iterator over elements of type `{integer}`
--> src/main.rs:5:45
|
5 | let y: Vec<dyn Display> = x.into_iter().collect();
| ^^^^^^^ value of type `Vec<dyn std::fmt::Display>` cannot be built from `std::iter::Iterator<Item={integer}>`
|
= help: the trait `FromIterator<{integer}>` is not implemented for `Vec<dyn std::fmt::Display>`
error[E0277]: the size for values of type `dyn std::fmt::Display` cannot be known at compilation time
--> src/main.rs:5:31
|
5 | let y: Vec<dyn Display> = x.into_iter().collect();
| ^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `dyn std::fmt::Display`
I can see a bunch of places where we could improve the output that we haven't gotten to yet:
- The third error shouldn't have been emitted in the first place, the first two are more than enough
- The first error has a note, but that note with a little bit of work could be turned into a structured suggestion for boxing or borrowing
- For the second suggestion we could detect this case in particular where the result would be !Sized and also suggest Boxing.
It is also somehow unfortunate that `impl Trait` in locals isn't yet in stable, but once it is it would let you write `let z: Vec<impl Display> = x.into_iter().collect();`, but as you can see here, that doesn't currently work even on nightly: https://play.rust-lang.org/?version=nightly&mode=debug&editi...
Listing all possible examples would indeed cause far too much output, but maybe the error should mention the possibility? Like: "If A and B are implementations of the same trait, you can use ...".