Skip to content

← Ownership III: Lifetimes, Explicitly step 6 of 22

Easy Primitives

Let elision do it

Four small string functions. The starter gives all four an explicit <'a>. Three of them do not need it. One of them does. Find the one.

pub fn longest_line(s: &str) -> &str
pub fn head_tail(s: &str) -> (&str, &str)
pub fn nth_field(s: &str, n: usize) -> &str
pub fn trim_prefix(s: &str, prefix: &str) -> &str   // <- one of these signatures is a lie

What each one does

  • longest_line — the longest line of s by byte length. Lines are split on \n. On a tie the first one wins. Empty input gives "".
  • head_tail — split at the first ASCII space: everything before it, and everything after it. No space at all means (s, "").
  • nth_field — split on , and return field number n, zero-indexed. Out of range gives "".
  • trim_prefix — if s starts with prefix, return the rest of s; otherwise return s unchanged.

The actual exercise

Elision assigns output lifetimes for you when it can. Rule 2 fires only when exactly one parameter carries a lifetime at all — then that lifetime goes to every elided output. Look at the four signatures with that rule in hand:

  • longest_line(&str) -> &str — one lifetime-carrying parameter. Elides.
  • head_tail(&str) -> (&str, &str) — still one. Both outputs get it. Elides.
  • nth_field(&str, usize) -> &strusize carries no lifetime, so there is still exactly one that does. Elides.
  • trim_prefix(&str, &str) -> &strtwo lifetime-carrying parameters. Rule 2 does not apply, the output lifetime is unknown, and you get E0106: missing lifetime specifier. This is the one you must annotate — and the annotation is a decision, not a formality.

Two lints are the graders here

Both are warn-by-default, and this problem compiles with -D warnings, so a warning is a hard failure:

  • clippy::needless_lifetimes — fires on an explicit lifetime that elision would have produced anyway. All three stray <'a>s in the starter trip it.
  • clippy::extra_unused_lifetimes — fires on a declared lifetime that is never used.

This is deliberate. “You usually don’t need annotations” is easy to nod along with and hard to act on; here it is mechanically enforced. Note that needless_lifetimes is not triggered by the annotation on trim_prefix — it stays quiet on lifetimes that genuinely link things elision could not link.

The call-site pin

At the bottom of the file:

const _: () = {
    fn _pin(s: &str) -> &str {
        let prefix = String::from("//");
        trim_prefix(s, &prefix)
    }
};

This is the point of the whole problem, so read it carefully. It borrows a local String as the prefix, and returns a reference derived from s, which came from the caller. That is a completely reasonable thing for a caller to want: the prefix was a scratch value, the result is a view into the real input.

Write trim_prefix<'a>(s: &'a str, prefix: &'a str) -> &'a str — the “just tie everything together” reflex — and it stops compiling with E0515: 'a is now forced to cover prefix too, so it can be no longer than a local that is about to be dropped. The signature compiled, the body worked, and the function was still wrong, because it over-promised on the caller’s behalf. Only trim_prefix<'a>(s: &'a str, prefix: &str) -> &'a str — “the answer comes from s, and prefix is nobody’s business but mine” — passes.

Do not edit or delete the pin. A submission without it is a failed submission.

Byte semantics, on purpose

longest_line compares len(), which is bytes, not characters. One of the hidden cases feeds you multibyte text so the difference is not academic. All the split points here (\n, ' ', ,) are ASCII, so every slice you produce lands on a character boundary.

Loading visualization…