Skip to content

← Ownership III: Lifetimes, Explicitly step 9 of 22

Medium Primitives

Two inputs, one output lifetime

The previous problem taught you to tie two inputs together with one 'a. This one teaches the opposite, and it is the more common case by a wide margin.

pub fn first_match<'a>(haystack: &'a [String], pattern: &str) -> Option<&'a str>
pub fn longest_common_prefix<'a>(a: &'a str, b: &str) -> &'a str

first_match returns the first element of haystack that contains pattern as a substring, or None. An empty pattern is contained in everything, so it matches the first element. longest_common_prefix returns the longest common prefix of a and b as a slice of a.

The design mistake this problem exists to prevent

Both functions get E0106 if you elide, because both have two lifetime-carrying parameters. The reflex fix — tie everything to one 'a — compiles:

pub fn first_match<'a>(haystack: &'a [String], pattern: &'a str) -> Option<&'a str>

It passes every runtime test. It is also wrong, and it is the number-one lifetime design smell in real crates. Ask what it promises: the returned reference is valid only over a region where both the haystack and the pattern are valid. But the returned string lives inside the haystack. The pattern was never involved. You have made every caller keep their search term alive for as long as they want to hold the result — for no reason at all.

Look at what the generated driver does:

let found: Option<&str>;
{
    let pattern = input.get("pattern").as_str().to_string();
    found = first_match(&haystack, &pattern);
}
// pattern is gone. `found` is still perfectly good.

That is a completely ordinary thing for a caller to write — build a scratch search term, use it, drop it, keep the hit. The over-tied signature makes it E0597: pattern does not live long enough, and the caller has no way to fix it short of restructuring their own code around your API’s imagination.

The correct rule is mechanical: give 'a only to the parameters the return value is actually made of. Everything else gets its own elided lifetime, which you never have to name.

The pins are the only enforcement here

const _: for<'a, 'b> fn(&'a [String], &'b str) -> Option<&'a str> = first_match;
const _: for<'a, 'b> fn(&'a str, &'b str) -> &'a str = longest_common_prefix;

Two binders, deliberately. A for<'a, 'b> function-pointer type demands a function that works for any pair of unrelated regions, so an over-tied submission fails to coerce with E0308: mismatched types right at the pin. A single-binder pin (for<'a> fn(&'a [String], &'a str) -> ...) would happily accept the wrong version — that is the trap, and it is why the pins look the way they do.

Note also what clippy::redundant_clone would not have caught: nothing here stops you cloning the haystack and returning an owned String… except that the pin says Option<&'a str>. Runtime assertions cannot see lifetimes. The pins are the grader. Do not edit or delete them.

The multibyte trap in longest_common_prefix

The obvious implementation walks bytes:

let n = a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count();
&a[..n]   // <- panics

Feed it a = "héllo", b = "hèllo". é is 0xC3 0xA9, è is 0xC3 0xA8. The first bytes of both are h and 0xC3, so n == 2 — which lands inside é. Slicing a str at a byte offset that is not a character boundary is a runtime panic, not a compile error, and one of the tests does exactly this. Iterate over chars (char_indices gives you the byte offset of each), and every index you produce is a boundary by construction.

Watch the empty-tail case too: if one string is a prefix of the other there is no mismatching character at all, and the answer is the shorter of the two — measured in bytes, since that is what you index with.

Loading visualization…