Skip to content

← Closures and Iterators step 9 of 28

Easy Primitives

map, filter, filter_map: parse host:port lines

Pick the usable ports out of a list of "host:port" lines.

pub fn parse_ports(lines: Vec<String>) -> Vec<u16>

Keep a line only if all three hold:

  1. it splits at a colon into a host part and a port part;
  2. the whole port part parses as a u16;
  3. the port is at least 1024 (below that is the privileged range).

Keep the surviving ports in input order, duplicates included. Everything else is silently dropped — no errors, no placeholders.

["localhost:8080", "example.com:80", "bad", "a:b"] gives [8080].

Split at the first colon and require the entire remainder to parse. That makes "a:b:c" and "a::8080" fail on their own, with no extra check: the remainders "b:c" and ":8080" are not numbers.

The three workhorses

These three cover most iterator code you will ever write or read.

Adapter Closure returns Item count Item type
map U unchanged TU
filter bool shrinks unchanged
filter_map Option<U> shrinks TU

filter_map is not a convenience wrapper. It is the right tool whenever deciding whether to keep an item and computing the kept value are the same work — which is exactly what parsing is. You cannot know whether "8080" is a valid port without parsing it, and once you have parsed it you would rather not throw the number away and parse it again.

Every filter_map closure returns Option<U>. Some(v) means “keep, and here is the value”; None means “drop this one”.

? inside the closure is the trick worth stealing

A filter_map closure returns Option, and ? works in any function or closure returning Option. So a chain of fallible steps collapses:

.filter_map(|line| {
    let (_host, port) = line.split_once(':')?;   // None -> drop
    let port: u16 = port.parse().ok()?;          // Err  -> drop
    // ... one more condition to go
})

split_once already returns Option<(&str, &str)>. parse returns Result, so .ok() converts it to Option and ? does the rest. Three early exits, no nesting, no match.

For the last condition, bool::then_some turns a predicate into an Option: (port >= 1024).then_some(port). Use then instead if the value is expensive to build — then takes a closure and is lazy, then_some takes a value and is eager. Clippy’s unnecessary_lazy_evaluations will tell you off for using then(|| x) where then_some(x) would do.

parse needs to be told what to parse into

let port = "8080".parse();          // E0283: type annotations needed

str::parse is generic over its return type: fn parse<F: FromStr>(&self) -> Result<F, F::Err>. Nothing in "8080".parse() says what F is, so inference has nothing to work with. Two ways to say it:

let port: u16 = s.parse().ok()?;    // typed binding
let port = s.parse::<u16>().ok()?;  // turbofish

The ::<> is the turbofish, and this is the first of many places you will need it. Item 9.21 is the general rule.

Parsing is strict, and the hidden cases lean on it: " 8080" fails (whitespace is not trimmed), "8080 " fails, "-1" fails (u16 is unsigned), "65536" fails (out of range), and "08080" succeeds as 8080 — leading zeros are fine.

The starter passes every test and is still rejected

Read it. It is the long way round, and it is what a filter_map-less mind produces:

.map(|line| line.split_once(':'))
.filter(|x| x.is_some())
.map(|x| x.unwrap())

It works. It is also two clippy errors:

error: `filter` for `Some` followed by `unwrap`   [clippy::option_filter_map]
error: `filter` for `Ok` followed by `unwrap`     [clippy::result_filter_map]

Both are default-on, so both are graded. And clippy is right for a reason worth internalising: filter(is_some) then map(unwrap) reintroduces a panic path that the type system had already eliminated. The two lines are correct only because they are adjacent. Move one, add an adapter between them, and you have a live unwrap on a None. filter_map makes that impossible to write.

While you are in there, filter(p).next() should be find(p) (filter_next, also default-on), and .map(|x| x) should be nothing at all (map_identity).

Write it as a single filter_map chain.

Grade is compile + tests + clippy -D warnings.

Loading visualization…