Skip to content
academia.sh

Lesson 08 / 14

Lifetime Elision Rules

Measures the rules the compiler uses to derive a lifetime annotation that is not written in a signature. The same signature is written and run in both explicit and implicit form, and both are shown to give the same behavior; a rejection-acceptance pair tests a signature elision does not cover, where the annotation stays mandatory.

Published Updated

In this lesson

  • Identifies the two signature forms lifetime elision covers
  • Runs the explicit and implicit forms of the same signature to show they give the same behavior
  • Shows with a rejection-acceptance pair that the annotation stays mandatory in a signature elision does not cover
Contents

The previous lesson measured when explicit annotation is required and what it expresses. But most of the functions seen in the course’s earlier lessons — ones that take a single input and return a reference, or methods that operate through self — carried no explicit annotation at all and still compiled. This lesson fills that gap: which rules the compiler uses to derive the unwritten annotation, and what happens in signatures those rules do not cover.

This question resembles the axis of the Go Fundamentals course — “what fills in for what is not written” — but it is not the same thing: what was measured there was where an unwritten type’s information was completed from by the compiler; what is measured here is where a reference’s life ends — elision rules only lift the requirement to write this information into the source, they do not change the information itself. The question the Go curriculum’s Concurrency course asked — “how does a running unit stop” — does not apply here either; this lesson still measures a reference’s validity range, not a unit of work stopping.

Elision itself is not a new source of information. The principle established in the previous lesson still holds: the compiler never guesses, it only shortens the writing in cases where it would always reach the same conclusion. When a signature form falls within elision’s scope, that means every signature of that form has exactly one correct reading; the path from input to output carries no more than one possibility. This lesson measures where that scope begins and where it ends.

This also has an important consequence for someone reading the source: if no annotation appears in a signature, that can mean two different things. Either the function has no reference relationship with the returned value at all, or the relationship exists but the signature form falls into one of the two patterns elision covers. The difference between these two is told apart by looking at the signature and checking the number of inputs and whether self is present — the two measurements below make this check concrete.

Three Rules, Two Conveniences

The relationship the compiler assumes when an annotation is not written in a signature is called elision. Rust’s language specification defines this assumption with three rules. The first rule gives every input reference its own separate, unnamed life — this always holds and produces no output on its own. The second rule, if the input has exactly one reference, assigns that input’s life to every elided output reference. The third rule, if &self or &mut self is among the inputs, assigns self’s life to every elided output reference.

The second and third rules are, in practice, the ones that make an annotation unnecessary — the first rule only names the input side, it ties the output to no input at all. For example, in a function that takes two references and returns no reference at all, only the first rule applies: each input gets its own separate life and the matter ends there, because there is no output to elide. The second and third rules only come into play when there is an elided output reference, and they determine which input that output is tied to.

The second rule’s “exactly one input” condition looks at the number of references among the inputs, not the number of inputs. If a function takes a number together with a reference, even though the input list carries two items, only one input carries a life — the second rule still applies. In the same way, if a function takes a single reference and returns a tuple containing multiple references, every elided output part gets the life of that same single input; the rule applies not to “a single output” but to “every elided output.” The two measurements below show these two conveniences separately: first a single-input function, then a method that takes &self.

Measurement: A Single-Input Signature

If a function takes only a single reference and returns a reference, the second rule comes into play: the life of the single elided input is assigned to the elided output. Below, the same function is written twice — once without an annotation, once with an explicit 'a — and both give the same result.

// elision_single_input.rs — single-input signature: same behavior even without the annotation.

struct Trace {
    name: String,
}

// Implicit: the compiler assigns the single input's life to the returned reference.
fn first_three_letters(trace: &Trace) -> &str {
    &trace.name[..3]
}

// Explicit: the same relationship written out by hand.
fn first_three_letters_explicit<'a>(trace: &'a Trace) -> &'a str {
    &trace.name[..3]
}

fn main() {
    let value = Trace { name: "scope-1".to_string() };
    println!("implicit -> {}", first_three_letters(&value));
    println!("explicit -> {}", first_three_letters_explicit(&value));
}
implicit -> sco
explicit -> sco

first_three_letters and first_three_letters_explicit carry the same body and give the same output; the only difference between them is whether the annotation is visible in the source. The compiler builds the same relationship for the first as it does for the second while compiling it — and there is no other possibility, because the input is single: the returned reference either comes from trace or does not exist at all.

LI6. Elision rules make an annotation unnecessary in cases where the compiler would always reach the same conclusion. In a single-input signature there is no real ambiguity about which input the returned reference will be tied to — there is already only one input.

This is seen more clearly next to the previous lesson’s primary_name function: primary_name could not be written without an annotation because it took two inputs, since whether the returned reference came from primary or from other could not be inferred from the source. first_three_letters takes a single input, so that same ambiguity never arises — the elision rule makes no real choice here, it only spares the source from writing down an option that was already the only one.

Measurement: A Method That Takes self

The third rule ties the elided output to self‘s life at all times, even if a method takes self along with other references. This explains why many of the methods seen in the course’s earlier lessons carried no annotation at all.

// elision_self.rs — a method taking &self: the returned reference ties to self even without the annotation.

struct Trace {
    name: String,
}

impl Trace {
    // Implicit: self's life is assigned to the returned reference.
    fn name(&self) -> &str {
        &self.name
    }
}

struct TraceExplicit {
    name: String,
}

impl TraceExplicit {
    // Explicit: the same assignment written out by hand.
    fn name<'a>(&'a self) -> &'a str {
        &self.name
    }
}

fn main() {
    let value = Trace { name: "scope-2".to_string() };
    let explicit = TraceExplicit { name: "scope-2".to_string() };
    println!("implicit -> {}", value.name());
    println!("explicit -> {}", explicit.name());
}
implicit -> scope-2
explicit -> scope-2

Trace::name and TraceExplicit::name give the same behavior. What stands out here is that the result would not change even if the method took another reference besides self — the third rule looks only at self, it does not take the other inputs into account at all. This is the same conclusion resting on a different foundation than the second rule: the compiler assumes that in a method taking self, the returned reference is almost always part of self, and this assumption was considered reliable enough to be turned into a rule.

Together, these two rules also explain why most of the methods seen in the course’s earlier lessons carried no annotation: methods that returned a reference to a type’s own field took &self, and the third rule came into play; free functions that read a value and returned a derived reference were mostly single-input, and the second rule came into play. Where an explicit annotation genuinely appeared, as in the previous lesson, was in signatures where multiple inputs were present together without self.

LI7. Two different rules produce the same outcome in two different signature forms: in single-input functions the input itself determines the output’s life; in methods taking self, self itself determines it. In both forms the signature is accepted even when no annotation is ever written; what the two have in common is that there is only one place the returned reference can go.

The Order for Reading a Signature

Together, the two measurements above also give an order for determining, by looking at a signature, whether an annotation is required. First, the number of references among the inputs is counted: if there are no references, or no output reference to elide, the first rule alone is enough and the matter is settled. If the input has exactly one reference, the second rule comes into play and the output is tied to it — first_three_letters takes this path. If the input has more than one reference, the next question is whether self is present: if it is, the third rule comes into play and the output is tied to self — Trace::name takes this path. If there is more than one reference and self is absent, none of the three rules can reach a single conclusion, and an explicit annotation becomes mandatory — longer_name falls into this last case.

This ordering is less about the compiler’s own inner workings and more a useful shortcut for a reader to quickly judge, by looking at a signature, whether to expect an annotation. Most of the functions seen through the course are settled in the first two steps; the places where an explicit annotation appears are exactly the signatures that fall into this ordering’s last step, the ones carrying more than one reference of equal weight.

This ordering itself requires no tool or checker — the three items can be memorized and applied by hand to any signature. This repeats the same pattern as the earlier measurements about lifetimes: the source, on its own, without being run, carries information that can be read. The difference is that what is read here is not a reference’s range but the decision of whether that range is written down or not.

Limiting Measurement: Elision Is a Convenience, Not a Rule

Elision rules are limited to three items, and there are signatures those three items do not cover. The clearest example is a function that contains no self and takes more than one reference: neither the second rule (no single input) nor the third rule (no self) comes into play. In such a function there is no single conclusion the compiler can reach on its own about which input the returned reference is tied to — the first rule gives every input its own separate life and assigns none of them to the output.

The two-reference, self-free function seen in the previous lesson (primary_name) was in exactly this situation and carried an explicit 'a — there, the annotation built a narrow promise tied to only one of the two inputs. The same boundary is tested here with a new example, this time with a situation where both inputs can genuinely be returned: a function that takes two Trace references and returns the slice of whichever name is longer is rejected by the compiler if written without an annotation, because the compiler cannot determine which input is returned. Depending on a runtime comparison, the body can return either x or y — which shows that the inputs are of equal weight, with neither taking priority over the other. The accepted form ties both to the same life with an explicit 'a.

// non_elided.rs — a signature elision does not cover: it does not compile without the annotation, it compiles once written.

struct Trace {
    name: String,
}

fn longer_name<'a>(x: &'a Trace, y: &'a Trace) -> &'a str {
    if x.name.len() >= y.name.len() { &x.name } else { &y.name }
}

fn main() {
    let a = Trace { name: "short".to_string() };
    let b = Trace { name: "a-longer-name".to_string() };
    println!("{}", longer_name(&a, &b));
}
a-longer-name

The unannotated form of longer_name matches none of the three rules: there are two inputs, and no self. The compiler rejects the signature because it cannot determine whether the returned reference comes from x or from y; the accepted form applies the same solution seen in the previous lesson’s other_name function — tying both inputs to the same 'a. The run above works with this accepted form and compares the two names, returning the longer one.

The difference between other_name and longer_name is not in the grounds for acceptance, but in when the return decision is settled: in other_name, which parameter would be returned was fixed in the function’s body; in longer_name, that decision is made fresh on every call, by a runtime comparison. In both cases the annotation does the same job — it covers every source that could possibly be returned in advance — because which branch will be chosen is not known at compile time; the annotation has to cover every possible path, not just the one actually taken.

LI8. Elision rules are a convenience, not a guarantee that the language can compile every signature. When none of the three rules fits a signature, the annotation is mandatory again; the rules only determine when it is not needed, not that it will never be needed.

This boundary rests on the same root as the principle established in the previous lesson: the compiler only skips an annotation when the source yields exactly one correct reading. In a function with two equally weighted inputs like longer_name, there is no single reading — x and y are of the same type, in the same position, and either can be returned. None of the three rules contains an assumption that would resolve this ambiguity, because any assumption could reject some correct programs while accepting some incorrect ones. Rather than take that risk, Rust rejects the code and leaves the decision to the developer; this shows why the annotation stays mandatory exactly where it functions as a guarantee, not a convenience.

Summary

  • Rust’s elision rules make writing a lifetime annotation unnecessary for some signature forms; two of these were measured in this course: single-input functions and methods that take self.
  • In a single-input function, the elided input’s life is assigned to the elided output; in a method taking self, this assignment is always made to self, regardless of the number of inputs.
  • The explicit and implicit forms of the same signature gave exactly the same behavior in both measured cases; in both forms, the values ended at scope end.
  • This lesson differs from the Go Fundamentals course’s axis, which measured unwritten type information: what is measured here is where a reference’s life ends, not where type information is completed from.
  • In signatures the three rules do not cover (multiple inputs, no self), the annotation stays mandatory; a signature outside their coverage is rejected if written without one.
  • Elision applies in cases where the compiler derives exactly one correct reading from a signature; when more than one equally weighted possibility exists, the compiler rejects the code rather than make an assumption.

Next Step

This lesson showed which signatures elision covers and which it does not — the annotation was unnecessary in the ones it covers, still mandatory in the ones it does not, and this necessity was established between a function’s parameters. But the same need can also arise in a struct’s own field: if a struct holds a reference, the struct itself cannot outlive the data it refers to, and this condition also needs to be written with an annotation. The next lesson measures this condition by comparing two designs that hold the same data, one by moving it into a struct, the other by borrowing it.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close