I realized that I never shared my lifetimes tutorial on Rust forums before.
The idea was to strip away all type machinery and provide an intuitive mental model of what lifetime analysis actually does and how it achieves memory safety, and then slowly reintroduce the type machinery back when explaining more complicated topics.
The project got abandoned years ago, only The Basics part was completed, but recently I made a quick update for Rust edition 2024 and I'm considering resurrecting the project if there is community interest in it.
Sorry, but you start your tutorial with a quite weak sentence:
Lifetimes are one of Rust’s most distinctive feature.
In my humble opinion, lifetimes are mostly a general property of variables, as most programming languages restrict valid access to data to specific scopes. Rust's actual contribution is more the borrow checker, controlling the validity of allowed borrow duration of references, and restricting invalid access and dangling pointers.
And one of the following sentences is a bit strange:
Region boundaries define where items assigned to the region can be accessed and for references - where the borrows of the referents end.
"items assigned to the region" seems to be wrong or at least very unclear.
[EDIT]
Perhaps replace "items assigned to the region" with "items declared within a scope"?
And the last part of that sentence, starting with "and", was a bit difficult to read for me. Perhaps there should be a colon after "accessed"?
In my humble opinion, lifetimes are mostly a general property of variables...
If only the Rust community had called lifetimes "regions" from the beginning... The status-quo is the definition of a lifetime is overloaded and depends on the context in which it is used. This sparks a lot of confusion and you may hear some wild claims akin to "we also have lifetimes in C++ and can perform the same static verification with external tools..." because of it. Is "The lifetime analysis" sounds better or should I use "The borrow checker"?
And one of the following sentences is a bit strange:
There are a lot of sentences with strange phrasings in this book, I'm not a native speaker and, at the time the book was written, there were no AI assistants to help me structure complex sentences, but this section in particular is overall much more complicated than it should be. Initially, I planned to describe all "mechanics" of the regions there but then I realized that it would be too much for this chapter and removed the descriptions without changing the introductory paragraphs.
While I'm trying to come up with a simpler explanation I recommend you to finish the chapter because it explains the same thing multiple times from slightly different perspectives and you may actually get it by the end 
Mr Quinedot and others use sometimes the term "borrow" duration, so I finally adopted this expression in my own Rust book.
, I'm not a native speaker and, at the time the book was written, there were no AI assistants
From the text quality, "Effective Rust" might be the best available Rust book currently. AI can help you a lot to improve the text quality, but you have to instruct it carefully, and it might take multiple passes, each requiring a carefully human proofreading.
1 Like
I have my own opinions on how to teach Rust borrowing
. In general I would recommend:
-
Don't conflate Rust lifetimes and lexical scopes / drop scopes / the liveness of values. Conflating these leads to incorrect reasoning like "this self-referencial type should work because the lifetime of the String is the same as the lifetime of the &str".
-
Don't use lexical scopes as an analogy for Rust lifetimes. In my experience it leads to too much confusion and makes it too easy to conflate Rust lifetimes and drop scopes.
-
Most common borrow checker errors can be explained in terms of what kept a borrow of some place (e.g. variable) alive and what use of that place was in conflict with being borrowed. For example, being moved or going out of scope are uses which conflict with being borrowed. As a more concrete example, your single lifetime impl Iterator error can be explained as "uses of the iterator and of the items keep (the referents of) both inputs borrowed (and being moved conflicts with being borrowed)".
And some more specific suggestions if you decide to revisit your tutorial:
-
I would start tackling function signatures with something less complicated than an -> impl Iterator<Item = &...>. Opaques (-> impl Trait) introduce invariance, have invisible generic capturing by default, are often assumed to have destructors which observe their captured borrows, etc.
-
When you do handle opaques, update the example to assume edition 2024+. On that edition, you would hit the solution as soon as you separated the lifetimes. uaf could stick around as a follow-up to explain that the iterator still keeps both inputs borrowed, but the items only keep one of the inputs borrowed.
-
Try some more bite-sized examples more generally. Your iterator example goes on for a long time, and then a "magic" solution is presented, with the explanation deferred until the next chapter on subtyping. (And I don't think many beginners would read those two chapters and conclude "Rust lifetimes are straightforward to work with".)
-
Pretty specific, but: when you introduce uaf your code starts using items you haven't shared (CrawlerOptions, crawler::new, do_stuff) and the reader loses their ability to easily follow along. I know it's a pain due to all the repetition, but try to make every actually-code code block runnable.
3 Likes
Well, this tutorial was written exactly because nobody explained lifetimes in terms of regions back in 2021(also, regions are not exactly scopes) but I found thinking about lifetimes in terms of regions very useful to design complex interfaces. The explanation in terms of borrowed places will make much more sense and will become much simpler after Rust switches to Polonius, especially with explicit lifetimes syntax and view types, but today it creates confusion for code samples that intuitively should compile but don't. With thinking in terms of regions you don't need to remember special cases when "the rules break".
The second reason this tutorial was written is the signatures like this one were very common in codebases I worked with so I wrote this book to save time explaining why they should be fixed.
Yeah, the target audience was the library authors, beginners were never considered 
Thanks for the feedback. It seems like the tutorial is not very helpful in its current shape and improving it requires a substantial rewrite, I've decided to leave it as is for now, it would be more productive to work on a rewrite explaining Polonius straight away instead.