Type errors are a part of library API surface and a fundamental shortcoming in Rust

I'm working on something in Dioxus and when I remove &* I get these error messages:

error[E0308]: mismatched types
   --> src/main.rs:174:13
    |
173 |         match venues {
    |               ------ this expression has type `generational_box::references::GenerationalRef<Ref<'_, std::option::Option<Vec<VenueItem>>>>`
174 |             Some(venues) => {
    |             ^^^^^^^^^^^^ expected `GenerationalRef<Ref<'_, ...>>`, found `Option<_>`
    |
    = note: expected struct `generational_box::references::GenerationalRef<Ref<'_, std::option::Option<Vec<VenueItem>>>>`
                 found enum `std::option::Option<_>`

Dioxus aside, nobody here can be serious that dealing with this kind of bizarre error messages should be what programming is about?

I think this is a fundamental problem in Rust and other type driven programming languages and it should be fixed. The computer knows the type system. There shouldn't be a real reason that I also have to run a type checker in my head for some dark API (GenerationalRef etc. is stuff that's under the hood) that I have no experience with.

Please fix this or at least consider the type errors to be part of your API surface when writing a library. Bad type errors lead to support burden on your side and effectively make a bad library.

I don't see anything "bizarre" about the current error, but that's more likely because I've become familiar with rustc's messaging than because it's actually good.

For the benefit of others like myself who have become desensitized to the problems, can you describe what sort of error message you'd prefer to see in this case, or otherwise have the compiler do differently?

4 Likes

Like in this case I don't see any connection between &* and the types in the error. If there is such a connection, it would be nice for the compiler to explain it. I guess some of the types implement something that's relevant here?

I'm trying to google up more common rustc error messages around & but can't find any.

In general I think there should be more tooling in rustc or on top of it to show the types it is considering and the state of mismatches. Printing out a barely readable line of noise with half a dozen >>>> is not really good enough.

2 Likes

If adding &* is the solution, then what's going on is almost certainly something implementing Deref leading to deref coercion.

Getting there from the current error message without already knowing the answer is admittedly nonobvious— You have to notice that GenerationalRef<...> is named like it's probably a smart pointer and that one of its type arguments matches the type of the other expression listed in the error (Option<...>).

Alternatively, you can pull up the documentation of GenerationalRef and start looking for methods that might give you the Option that you need. In this case, the only things implemented are Display, Debug, and Deref so you can probably infer that you'll need to use the deref operator * to get anything useful out of it.

Ideally, the compiler should at a minimum notice that there's a Deref implementation on one of the types involved in the mismatch and issue a help message to point you in that direction.

14 Likes

To me the error is good as it stops a bug in code (that would otherwise be hard(/take a long time) to find) and gives me where I should be looking in the documentation. GenerationalRef of generational_box crate. (With Ref giving a big hint.)

&* on the other hand I'm not the biggest fan on but it is a core part of the language so not going anywhere. You just get used to it and it becomes second nature.

The problem; it being prefix operator and suffix operators have higher precedence; so your reading has to adjust (which isn't as easy is as just going in a single direction.)

Same for lifetimes: If I can try different lifetime options until I get one that compiles (because realistically who knows what's going to work here), why can't the computer do this for me?

That's a bad way to proceed. You have to comprehend the language. (It takes time, trial and error helps only to a degree in learning.) Computers are dumb (especially the current AI.) They are fantastic at giving something that appears to work but lack any know-how whether such answer is correct.

8 Likes

I'm not sure what you mean here. The >>>> you're complaining about is the (only) type that the compiler is "considering". It's not the prettiest type (dioxus could do a better job there by flattening the type and giving it a better shorter name, but it wouldn't change much for your error).

If you're doing that I have to say that you're doing it wrong.

It might be because I've worked with Rust for some years, but I find most common usages of lifetimes pretty intuitive.

It could, but it would be painfully slow as there are an exponential amount of possibilities. It would also be pretty bad for incremental compilation, since now the signature of a function depends on its body, and to borrow check that it needs the signature of other functions etc etc. It's easy to create cycles with this, which are very tricky to handle. Not to mention you would have no insight into what is happening.

It's part of Rust's design that all signatures should be made explicit to avoid these kind of problems.

7 Likes

I could probably figure this out but I'm pretty sure that lifetimes are a stumbling block for most Rust beginners (and rightly so).

Exponential growth among the handful of types that are realistically under consideration should be relatively easily searched on modern computers.

The alternative is either for LLMs to write this stuff for us or for better tools and visualizers to build on top of it.

They might be able to, but that doesn't necessarily mean that they should. Take this incorrectly implemented max function:

fn max(x: &i32, y: &i32) -> &i32 {
    // TODO: fix this
    x
}

Currently this is an error (playground link: Rust Playground), and actually suggests the correct signature:

help: consider introducing a named lifetime parameter
  |
1 | fn max<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  |       ++++     ++          ++          ++

However, if the compiler was allowed to infer it, it might instead decide to infer this signature instead:

fn max<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32

Which would be valid for the function's incorrect implementation, but if someone changed the body to be correct...

fn max(x: &i32, y: &i32) -> &i32 {
  if x > y { x } else { y }
}

...then this would be a breaking change, since the compiler would have previously inferred that the return value would be a valid borrow for the lifetime of x, but would now be forced to infer that it would only be valid where x and y are valid. A function's "effective" signature changing without the signature written in the code changing would be really bad. No matter what you do, you can't infer all lifetimes without global inference (which Rust has intentionally avoided). It might be possible/reasonable for some more cases to be inferred though.

12 Likes

A graphical one could be a good tool for someone wanting a project challenge and not sure what to do. (Of a slightly more difficult nature.) Text only descriptions of lifetimes have been lacking.

Not sure it would be much use out of learning but I could be proved wrong.

1 Like

Isn't this contrived? Most of the time we have correct bodies with failing inference, not the other way round.

Would it really be "really bad"? I guess it's something that you should prevent but maybe not the end of the world?

What's really bad is trying to explain lifetimes or the rust project organisation situation.

Type errors are a part of library API, yes. This is one of the biggest and most important benefits of Rust!

That said, an API giving incomprehensible type errors is not a good API, but that is a problem of the specific API (the crate) not the language or the compiler. At the same time, this aspect of API design is still rather unusual and maybe not widely known. Also, there is also some specific design tools for it, of wich some are rather new (e.g. #[diagnostic::on_unimplemented]) and therefore not yet used by some crates that would benefit from it, and other not yet stabilized or even written.

So while I don't agree that the current situation is that bad, I'm certain it will improve! :cowboy_hat_face:

6 Likes

Filed Suggest `&*` when appropriate · Issue #132784 · rust-lang/rust · GitHub for this.

24 Likes

Yes, but simple examples are easier to understand.

One could also say that most of the time C programmers remember to call free() :wink:
You might be right there, but Rust tends to try to prevent mistakes as much as possible rather than make it easy as long as you don't make mistakes. I think you do make a good point that libraries should try to make these mistakes easier to deal with. However, in my opinion the correct way to go about this is to have better error messages (whether by adding across-the-board improvements or annotations to add specialized error messages).

I would say that yes, having a breaking API change without changing the appearance of the API is extremely bad.

It would be possible to default to functions that take and return references having their return values be bound by the lifetimes of all the reference arguments. I think that might not be a good idea, because it's often wrong and requiring the lifetime bounds anyway makes APIs more explicit, but that is just my opinion.

To also note, "deref patterns" would theoretically solve this specific example as well. If deref must be explicit in patterns, then the compiler should notice and suggest using k#deref Some(venues) as the pattern (syntax deliberate placeholder), and if deref participates in default binding modes (often known as match ergonomics), then the OP example code would even just work.

This is a case where a convenience feature (default binding modes) widened the gap between native references and custom reference-like types. In the before-times, you would've needed to write

match *venues {
    Some(ref venues) => {

even for &Option<Vec<VenueItem>>, so choosing to use Ref<Option<Vec<VenueItem>>> would've been a smaller delta to the API consumer experience.

4 Likes

You're assuming that &* and type mismatches are somehow strongly coupled. They're not. It's a far more general "this needs A, you have a B" problem and there is something that lets you go from B to A, but that path might be a method call (perhaps even of a trait method that not even in scope), a deref, a cast, or something more complicated.

The compiler can and does try to guess some of these for you and put them in the suggestions under the error. But those suggestions can be wrong, which is one of the reasons they're not applied automatically. And in the end it cannot perform an unbounded search to figure out how you might have meant to get from B to A, you need to spell it out.
Perhaps there could be a separate tool that searches through the possibilities and presents them incrementally as it finds them, such a tool could even be LLM-guided as you say, but that then is not the compiler's job, but some coding assistant's.

3 Likes

I'm of two minds on this issue. First, rustc is definitely not perfect when it comes to diagnostics, even if it's often really good at providing suggested solutions to common problems. This might lead to expectations that it should be just as good in all situations.

A recent example appeared in Overflow evaluating the requirement in a Iterator. Where the compiler was creating an infinitely recursive type from a recursive chain of Iterator adapters. (The code in OP never evaluates the chain of iterator adapters.) The error message is definitely unreadable, a lot like what we used to experience with Future adapters before async fn raised the abstraction level. These problems do happen in practice!

On the other hand, &*foo exists for good reasons [1]. This is not a fundamental shortcoming. It's a feature!

While better diagnostics could be helpful, I don't know what could be realistically accomplished by suggesting the library authors reconsider their API. Presumably they have good reasons for exposing their own reference types. I am not interested in digging into these reasons, but this is a good stance to start from.

If you can prove that these references are not needed and it results in a notable improvement for library users, submit a PR! Or worst case if it's rejected, fork the library.


  1. See Chesterton's fence for why it is important to recognize that things exist for a reason, even if you are unaware of what that reason is. ↩︎

3 Likes

pull up the documentation of GenerationalRef

Generational Box is a runtime for Rust that allows any static type to implement Copy.
It can be combined with a global runtime to create an ergonomic state solution like dioxus-signals

Those two sentences seem totally unrelated. With enough reading, it seems that this addresses some problem created by the async system. The documentation does not mention this.

They read to me relatively clearly: it's saying that it's part of a system for avoiding the user needing to clone() everything everywhere by implementing something of a garbage collector? With such a ref, this can't be a general solution, but presumably clever API design means they don't need anything more than knowing that a reference is stale, eg trying to update a response that's already sent.