Skip to content

← Ownership III: Lifetimes, Explicitly step 15 of 22

Easy Primitives

Make the lint happy: '_ in return paths

This exercise has four one-line functions that return iterators from the standard library. Their bodies are already correct. Each one still fails the Rust gate, even though none of the signatures names a lifetime.

pub fn bytes_of(data: &[u8]) -> slice::Iter<u8>       // rejected
pub fn letters(s: &str)      -> str::Chars            // rejected
pub fn fields(s: &str, sep: char) -> str::Split<char> // rejected
pub fn pairs(v: &[i64])      -> slice::Chunks<i64>    // rejected

Your job is to fix the four return types, then write run, which calls all four functions and collects their results.

What a lifetime parameter is doing here

A lifetime is the compiler's name for the region of code in which a borrow is valid. Each of these iterators borrows from its input. For example, data.iter() yields &u8 references that point into data, so the iterator must not outlive data. That is why std::slice::Iter is declared as Iter<'a, T>: the 'a parameter records which borrow the iterator depends on. Chars<'a>, Split<'a, P> and Chunks<'a, T> have a lifetime parameter for the same reason.

When you leave a lifetime out of a function signature, the lifetime elision rules fill it in. Here each function has exactly one borrowed input, so the rules connect the returned iterator's lifetime to that input. The starter signatures already mean the right thing. The problem is only how they are written.

Three ways to write the same lifetime

The compiler groups the ways a lifetime can appear in a signature:

group what you write examples
named a lifetime name &'a str, Person<'a>
elided a visible placeholder or a bare & &str, &'_ str, Person<'_>
hidden nothing at all, in a type path that has a lifetime parameter Person, Iter<u8>

A type path such as slice::Iter<u8> is allowed to omit its lifetime argument entirely, so -> slice::Iter<u8> means the same as -> slice::Iter<'_, u8>, with the lifetime invisible.

The rustc lint mismatched_lifetime_syntaxes fires when one lifetime is written using syntax from different groups within a single signature. It is a warning by default (it was added in Rust 1.89). The Rust gate on this site runs Clippy with -D warnings, which turns every warning into an error, so this lint rejects the submission. For bytes_of, Rust 1.98 reports:

warning: hiding a lifetime that's elided elsewhere is confusing
help: use `'_` for type paths

In bytes_of(data: &[u8]) -> slice::Iter<u8>, the input's lifetime is in the elided group (&[u8]) and the output's is in the hidden group (Iter<u8>). Writing slice::Iter<'_, u8> puts both in the elided group, and the lint no longer fires.

The lint compares groups, not exact spelling. fn f(x: &u8) -> &'_ u8 does not trigger it, because &u8 and &'_ u8 are both in the elided group.

'_ does not mean "no lifetime"

'_ means "a lifetime belongs here; apply the usual rules to choose it." In these signatures the elision rules pick the input's lifetime, exactly as they did when the lifetime was hidden. Adding '_ changes how the signature reads, not what it means.

Rust does not require '_ in every path. A separate lint, elided_lifetimes_in_paths, reports any hidden lifetime, but it is allow-by-default (its documentation notes known issues with it), so it is off unless a crate turns it on. Only mixing groups within one signature is reported by default.

Other accepted fixes

Naming the lifetime also satisfies the lint, and it passes this gate:

pub fn bytes_of<'a>(data: &'a [u8]) -> slice::Iter<'a, u8>   // accepted

Both positions are then in the named group. With Rust 1.98, Clippy's default lints accept this signature. The Clippy lint that suggests replacing such a name with '_, clippy::elidable_lifetime_names, is in the pedantic group, which this gate does not enable. '_ is the shorter fix and the one rustc's help message suggests, but either form passes.

You could also return impl Iterator<Item = &u8> (with or without + '_), which passes as well. That is a different design choice: it hides the concrete iterator type from callers, so they can use only the Iterator methods. For this exercise, keep the concrete types and fix the lifetimes.

Where the lifetime argument goes

Lifetime arguments come before type arguments. str::Split is declared Split<'a, P>, so the fix is Split<'_, char>. Writing Split<char, '_> is rejected with error E0747 ("type provided when a lifetime was expected"), and Split<'_> is rejected with E0107 because the type argument is missing.

The entry point

The tests exchange JSON, which cannot represent an iterator, so run collects each one into a vector:

pub fn run(v: Vec<i64>, s: String, sep: String)
    -> (Vec<u8>, Vec<String>, Vec<String>, Vec<Vec<i64>>)

Return, in this order:

  1. bytes: the UTF-8 bytes of s, in order (bytes_of).
  2. letters: each char of s as its own one-character String (letters). A Rust char is a single Unicode scalar value, so a multi-byte character such as é is one entry here but two or more bytes in the first list. A letter written as e plus a combining accent is two entries.
  3. fields: s split on the first character of sep, exactly as str::split does (fields). Any further characters in sep are ignored. Adjacent separators, or a separator at the start or end, produce empty fields, and an empty s produces one empty field (""), not zero fields. The tests always pass a nonempty sep.
  4. pairs: v in consecutive chunks of two, in order (pairs). If v has an odd length, the last chunk has one element; an empty v gives no chunks.

For example, run(vec![1, 2, 3, 4, 5], "a,b".into(), ",".into()) returns bytes [97, 44, 98], letters ["a", ",", "b"], fields ["a", "b"] and pairs [[1, 2], [3, 4], [5]].