Skip to content

← Closures and Iterators step 11 of 28

Easy Primitives

take_while and skip_while: split a message at the blank line

Split a list of lines into a header and a body at the first blank line.

pub fn header_body(lines: Vec<String>) -> (Vec<String>, Vec<String>)

A line is blank if it is empty or contains only whitespace. The blank line that does the splitting appears in neither half. Every other line — including every subsequent blank line — is kept exactly where it was.

["Subject: hi", "From: me", "", "Hello", "", "World"]
  -> header: ["Subject: hi", "From: me"]
     body:   ["Hello", "", "World"]

Edge cases, all tested:

  • no blank line at all → everything is header, body is empty;
  • a leading blank line → header is empty;
  • two blanks in a row → the first splits, the second is body content;
  • empty input → two empty vectors.

Build both halves with take_while, skip_while and skip. No len() arithmetic, no index loops, no position().

take_while is not filter, and this is where people lose an afternoon

Both take a predicate. Both return fewer items. They are completely different, and confusing them produces silently wrong output rather than a compile error — no rustc diagnostic, no clippy lint, just a test failure weeks later.

What it does
filter(p) Tests every item. Keeps the ones that pass. Never stops early.
take_while(p) Yields items while p holds. At the first failure it stops for good, and never calls p again.
skip_while(p) Skips items while p holds. At the first failure it stops skipping, and yields everything from there on — including later items that would have passed p.

Neither take_while nor skip_while ever resumes. They each flip once.

The starter builds the body with filter, and it gets the right answer whenever there is exactly one blank line in the whole input — which is why this bug survives casual testing. Feed it two blanks and the second one disappears from the body. filter has no notion of “the first one”; it cannot express this problem at all.

The item that take_while eats

Here is the detail that makes the skip(1) necessary, and that item 9.13 turns into a whole problem.

When take_while‘s predicate returns false, that item has already been pulled out of the source iterator. take_while consumed it in order to test it, and then dropped it on the floor. If you were sharing one iterator between two phases, that element would simply be gone — this is why parsers built on plain take_while mysteriously lose one token per phase, and why by_ref and peekable exist.

Here you walk the input twice from the start, so nothing is lost. But skip_while stops skipping at the blank line without consuming it, so it yields the blank line first — hence the .skip(1) to drop the separator. Get that off by one and case t3 (leading blank) will tell you immediately.

The rest of the family

  • take(n) / skip(n) — count-based, not predicate-based. skip(n) on a shorter iterator yields nothing rather than panicking.
  • step_by(n) — every n-th item, always including the first. (0..10).step_by(3) is 0, 3, 6, 9.

Three of clippy’s lints here are deny-by-default, which is unusual and tells you they catch real bugs rather than style: iter_skip_zero (.skip(0) does nothing — you meant .skip(1)), iterator_step_by_zero (.step_by(0) panics at runtime), and infinite_iter (a chain that provably never terminates). Also default-on and worth knowing: iter_skip_next.skip(1).next() is .nth(1).

Cloning

Both halves are owned Vec<String>, built from a vector you were handed by value. Filter first, clone the survivors — .take_while(..).cloned(), not .cloned().take_while(..), or clippy’s iter_overeager_cloned will point out that you cloned strings you then threw away.

Grade is compile + tests + clippy -D warnings.

Loading visualization…