Mismatched lifetime syntaxes lint in Rust 1.89.0

Sorry, this is probably a rant. But I also have a question at the end.

I think the newly enabled "Mismatched lifetime syntaxes lint" in Rust 1.89.0 is making Rust code less readable.

Quote the example from the release blog:

fn items(scores: &[u8]) -> std::slice::Iter<u8> 

will cause the lint warning, and will need to be changed into

fn items(scores: &[u8]) -> std::slice::Iter<'_, u8>

As a regular Rust user, I felt the first one is more readable, and the new explicit lifetime is not worth it. With today's IDEs, it is very easy for users to be aware of the definition of Iter has a lifetime.

Isn't the whole point of "Lifetime elision" is to make the code more readable? This change seems to be going backwards.

My question is: Is there a real problem with using Iter<u8> ?

3 Likes

Not every place that people read Rust code is an IDE. This forum is not. Discord is not. Zulip is not. GitHub Gist or other “pastebin” site is not. git diff is not. And an IDE can’t fully help you with code that doesn’t compile yet.

I spend a lot of time helping people with Rust problems, including borrow checking problems. New users frequently write out a type signature they have in mind and have no idea that they’ve introduced a borrowing relationship between the function’s input and output — and when I’m trying to help them, I need to ask them “please tell me the definitions of the types your function mentions” to see which of them, if any, has a lifetime in it. I don’t have an IDE pointed at their code. Following this lint will solve all of that, because it will make it syntactically obvious whether a lifetime relationship exists or not.

The important case isn't a relatively well-known type like -> std::slice::Iter<u8>, it's -> Foo<u8>. Look at that return type. Does it borrow anything? You don’t know, because I haven’t told you what Foo is. With code that doesn’t trigger mismatched_lifetime_syntaxes, you can know.

22 Likes

A "me too" in support of the lint. To date, the first thing I do when helping someone with a borrow check error involving functions is to add #[deny(elided_lifetimes_in_paths)].[1] The elision is write-only ergonomics that makes things worse for readers who don't know all the context (all the types). I.e. less readable in terms of comprehension (even if it is prettier).

Long ago, the blog phrased it like so:

Context-dependence: here, we overshot. The fact that elision applies to types other than & and &mut, means that to even know whether reborrrowing is happening in a signature like fn lookup(&self) -> Ref<T>, you need to know whether Ref has a lifetime parameter that's being left out. For something as common as function signatures, this is too much context. We've been considering pushing in the direction of a small but explicit marker to say that a lifetime is being elided for Ref

We got the explicit marker ('_) a long time before the lint was tuned to a level considered acceptable to enable by default.


  1. with the refined lints, this may change or go away ↩︎

15 Likes

Adding to the existing responses, there's a difference in readability and understandability. fn x() is easier to read than fn erase_my_entire_hard_drive_with_no_chance_of_recoverability(), but I'd say the latter is more understandable.

I'll echo kpreid that this is an actual problem that I see quite frequently in newcomers (e.g. in Discord or when I do Rust training). As a regular Rust user, you may know that Iter contains a lifetime, but those newcomers don't. Add to that the existing point about not knowing if SomeArbitraryTypeFromACrate contains one or more lifetimes, and it won't matter if you are an experienced Rust user because you might not be an experienced some_crate user.

As an amusing anecdote, this isn't even limited to "new users" or "new crates". I have a specific memory burned into my brain of talking to Niko Matsakis about something in the standard library and he was surprised that that type had a lifetime. If Niko counts as a newcomer and/or the standard library counts as an obscure crate, we are all doomed.

I do the same, and even encourage people to set it up as deny-by-default in every new project. elided_lifetimes_in_paths is overly aggressive, however, in that it requires you to mark the lifetime in fn foo(_: SomeType<'_>). Here it's not as valuable because that lifetime isn't tied to anything.

As of Rust 1.89, you can now mostly[1] skip adding this lint.


  1. There's still the case of fn(ContainsLifetime) -> ContainsLifetime which needs help from the still-to-be-added hidden_lifetimes_in_output_paths lint. Hopefully that will also be marked as warn-by-default. ↩︎

8 Likes

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.