gear idea
Possible Rust

Learning what’s possible in Rust.
Jump to Navigation

Naming Your Lifetimes

May 20th, 2021 · Pattern · #lifetimes

You may not know this, but it’s possible to give names to your lifetimes which are longer than a single character! Effectively naming your lifetimes can help improve code clarity in several scenarios, which this post describes in detail.

If you’ve looked at example Rust code, you’ve likely noticed that lifetimes are usually named with a single letter. In your first introduction to lifetimes, where they’re revealed explicitly with a transition from an inferred-lifetime code sample to one with explicitly named lifetimes, you might have seen something like the following.

Skip this content.
struct Person {
    name: String
}

impl Person {
    pub fn name<'a>(&'a self) -> &'a str {
        &self.name
    }
}
Code 1

A common type of first lifetime code sample.

This code example isn’t wrong, and there are certainly a lot of single-letter lifetime parameter names in real-world Rust codebases, but the tendency to use these short names in example code can leave some Rustaceans with the impression that only single-letter names are allowed, but in fact they can be any length at all! Here’s another version of the above.

Skip this content.
struct Person {
    name: String
}

impl Person {
    pub fn name<'me>(&'me self) -> &'me str {
        &self.name
    }
}
Code 2

A rewrite of the prior example, with a lifetime variable name that’s possibly more helpful.

note Info 1 A contrived example.

Skip this content.

This example is a bit contrived and may not be considered sufficiently useful in terms of the additional explanatory value of the longer name, but helps to illustrate the idea that names can be longer.

There are specific scenarios where effective naming can be helpful, including in the presence of long-lived common owners whose values are borrowed throughout the code (like an allocation arena or configuration struct), or in the presence of multiple borrow sources you want to clearly disambiguate.

Case 1: Long-Lived Common Owner

It’s common in many programs to have a single struct which contains a variety of data used throughout a program. It may be a configuration struct holding information on how the program was initialized or set to run. It may be an arena holding data pulled from outside sources. Whatever the case may be, the pattern of centralizing data and then handing out borrows to that data is common, and for good reason. It can reduce data duplication and lead to clearer-to-navigate code.

Skip this content.
use once_cell::unsync::OnceCell;

struct Provider {
    data_cache: OnceCell<Data>,
    metadata_cache: OnceCell<Metadata>,
}

// ...

fn process_data(data: &Data) -> Result<&str> {
    // ...
}
Code 3

An example of a central data provider with a function processing data from it.

This example shows an imaginary “data provider,” containing some caches of information used throughout a program. It also shows a process_data function which is operating on data borrowed from the provider. Now, in this case the process_data function can be written as-is, without specifying lifetimes explicitly; the fact that there’s only one input lifetime means the output reference is automatically inferred to have the same lifetime as the input reference. However, this process_data function doesn’t make clear that data is coming from the Provider, and an unwitting developer may attempt to use the data in a way which outlives the Provider. This may be made less likely with appropriate naming.

Skip this content.
fn process_data<'prov>(data: &'prov Data) -> Result<&'prov str> {
    // ...
}
Code 4

A rewrite with explicit lifetimes.

This example works the same as the prior one, but the use of the 'prov name for the lifetime helps hint to future developers that this data is coming from the Provider.

This kind of situation also commonly arises with arena allocators. Inside the Rust compiler, for instance, there’s the extremely common 'tcx lifetime, which is the lifetime of the arena containing the typing context for the program. This lifetime appears all over the Rust compiler codebase, and whenever you see 'tcx, you’re provided with information about where the reference is coming from, and how long it’ll live.

Case 2: Multiple Borrow Sources

Sometimes, you may alternatively have a structure which contains borrows of multiple sources. In the following example, we use a “view” structure to combine references to multiple places into a single structure.

Skip this content.
struct Article {
    title: String,
    author: Author,
}

#[derive(PartialEq, Eq)]
struct Author {
    name: String,
}

struct ArticleProvider {
    articles: Vec<Article>,
}

struct AuthorProvider {
    authors: Vec<Author>,
}

struct AuthorView<'art, 'auth> {
    author: &'auth Author,
    articles: Vec<&'art Article>,
}

fn authors_with_articles<'art, 'auth>(
    article_provider: &'art ArticleProvider,
    author_provider: &'auth AuthorProvider,
) -> Vec<AuthorView<'art, 'auth>> {
    author_provider
        .authors
        .iter()
        .map(|author| {
            let articles = article_provider
                .articles
                .iter()
                .filter(|article| &article.author == author)
                .collect();

            AuthorView { author, articles }
        })
        .collect()
}
Code 5

A view structure bundling multiple references together.

In this example, the “view” structure has two lifetimes to permit the borrows of the respective fields to come from different places, and the naming with longer names helps to disambiguate which borrow is which, and where it’s coming from.

This example also wouldn’t work without explicitly specifying the lifetimes with some sort of name, as the lifetime inference rules do not infer lifetimes for output references when more than one input reference exists. This is because there’d be no principled way to infer which input lifetime the output lifetime is derived from without inspecting the body of the function, and for performance and clarity reasons, the Rust compiler declines to do that sort of in-depth function body analysis.

Conclusion

There are more cases than these where you might want to use longer lifetime names. They’re not always appropriate, but when they are, it’s good to know that they’re possible. Questions of readability are always trade-offs between length and clarity, and there’s no hard and fast rule for what’s “right.” Hopefully this post has at least helped clarify the situations when you might want to apply this practice.

Possible Rust succeeds when more people can join in learning about Rust! Please take a moment to share, especially if you have questions or if you disagree!

Share on Twitter

Discussions