910-iterator-find-map — Iterator find_map
Tutorial
The Problem
A common pattern: try a transformation on each element, take the first success, ignore failures. Parsing the first valid integer from a list of strings, finding the first key=value pair in a config, finding the first element longer than a threshold — all follow this pattern. The naive approach uses filter_map(f).next(), but find_map(f) expresses the intent more directly: "find the first element for which f returns Some." OCaml's List.find_map was added in 4.10. Haskell's Data.Maybe.mapMaybe and listToMaybe . mapMaybe f serve the same role. It is the "optional value from the first successful transformation" operation.
🎯 Learning Outcomes
.find_map(f) to find the first Some(...) result in one pass.filter_map(f).next()find_map_rec.find(pred) (predicate, returns element) vs .find_map(f) (transform, returns transformed value)Code Example
pub fn first_int(strings: &[&str]) -> Option<i32> {
strings.iter().find_map(|s| s.parse::<i32>().ok())
}Key Differences
find_map was added to OCaml in 4.10; Rust has had it since 1.30. Both are now standard.filter_map(f).next() is equivalent but less intent-revealing than find_map(f); same tradeoff in OCaml.Some result without evaluating the rest..find(pred) returns Option<&T> (the element); .find_map(f) returns Option<U> (the transformed value) — more general.OCaml Approach
List.find_map: ('a -> 'b option) -> 'a list -> 'b option (since 4.10) is the direct equivalent. Before 4.10: let find_map f xs = match List.filter_map f xs with [] -> None | x :: _ -> Some x (inefficient) or recursive manual implementation. List.find_opt: ('a -> bool) -> 'a list -> 'a option is the simpler case (no transformation). For sequences: Seq.find_map is available in OCaml 5.1+.
Full Source
#![allow(clippy::all)]
//! 271. Transform-and-Find with find_map()
//!
//! `find_map(f)` finds the first `Some(...)` result — single pass, lazy.
//! Equivalent to `filter_map(f).next()` but expresses intent more clearly.
/// Parse the first valid integer from a slice of strings.
pub fn first_int(strings: &[&str]) -> Option<i32> {
strings.iter().find_map(|s| s.parse::<i32>().ok())
}
/// Find the first string longer than `min_len` and return its length.
pub fn first_long_len(strings: &[&str], min_len: usize) -> Option<usize> {
strings.iter().find_map(|s| {
if s.len() > min_len {
Some(s.len())
} else {
None
}
})
}
/// Parse the first `key=value` entry from a slice of config-style strings.
pub fn first_kv<'a>(entries: &[&'a str]) -> Option<(&'a str, &'a str)> {
entries.iter().find_map(|s| s.split_once('='))
}
/// Recursive implementation mirroring OCaml's `List.find_map`.
pub fn find_map_rec<T, B, F>(list: &[T], f: F) -> Option<B>
where
F: Fn(&T) -> Option<B>,
{
match list {
[] => None,
[head, tail @ ..] => f(head).or_else(|| find_map_rec(tail, f)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_first_int_found() {
let strings = ["hello", "42", "world", "17"];
assert_eq!(first_int(&strings), Some(42));
}
#[test]
fn test_first_int_none() {
let strings = ["hello", "world", "foo"];
assert_eq!(first_int(&strings), None);
}
#[test]
fn test_first_int_empty() {
assert_eq!(first_int(&[]), None);
}
#[test]
fn test_first_long_len() {
let strings = ["hi", "hello", "world", "rust"];
assert_eq!(first_long_len(&strings, 4), Some(5));
}
#[test]
fn test_first_long_len_none() {
let strings = ["hi", "yo", "ok"];
assert_eq!(first_long_len(&strings, 4), None);
}
#[test]
fn test_first_kv_found() {
let entries = ["BAD", "PATH=/usr/bin", "HOME=/root"];
assert_eq!(first_kv(&entries), Some(("PATH", "/usr/bin")));
}
#[test]
fn test_first_kv_none() {
let entries = ["noequals", "alsonone"];
assert_eq!(first_kv(&entries), None);
}
#[test]
fn test_find_map_rec() {
let nums = [1i32, 2, 3, 10, 4];
let result = find_map_rec(&nums, |&n| if n > 5 { Some(n * 2) } else { None });
assert_eq!(result, Some(20));
}
#[test]
fn test_find_map_rec_none() {
let nums = [1i32, 2, 3];
let result = find_map_rec(&nums, |&n| if n > 100 { Some(n) } else { None });
assert_eq!(result, None);
}
#[test]
fn test_find_map_vs_filter_map_next() {
// find_map is equivalent to filter_map(...).next()
let strings = ["a", "2", "b", "3"];
let via_find_map = strings.iter().find_map(|s| s.parse::<i32>().ok());
let via_filter_map = strings.iter().filter_map(|s| s.parse::<i32>().ok()).next();
assert_eq!(via_find_map, via_filter_map);
assert_eq!(via_find_map, Some(2));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_first_int_found() {
let strings = ["hello", "42", "world", "17"];
assert_eq!(first_int(&strings), Some(42));
}
#[test]
fn test_first_int_none() {
let strings = ["hello", "world", "foo"];
assert_eq!(first_int(&strings), None);
}
#[test]
fn test_first_int_empty() {
assert_eq!(first_int(&[]), None);
}
#[test]
fn test_first_long_len() {
let strings = ["hi", "hello", "world", "rust"];
assert_eq!(first_long_len(&strings, 4), Some(5));
}
#[test]
fn test_first_long_len_none() {
let strings = ["hi", "yo", "ok"];
assert_eq!(first_long_len(&strings, 4), None);
}
#[test]
fn test_first_kv_found() {
let entries = ["BAD", "PATH=/usr/bin", "HOME=/root"];
assert_eq!(first_kv(&entries), Some(("PATH", "/usr/bin")));
}
#[test]
fn test_first_kv_none() {
let entries = ["noequals", "alsonone"];
assert_eq!(first_kv(&entries), None);
}
#[test]
fn test_find_map_rec() {
let nums = [1i32, 2, 3, 10, 4];
let result = find_map_rec(&nums, |&n| if n > 5 { Some(n * 2) } else { None });
assert_eq!(result, Some(20));
}
#[test]
fn test_find_map_rec_none() {
let nums = [1i32, 2, 3];
let result = find_map_rec(&nums, |&n| if n > 100 { Some(n) } else { None });
assert_eq!(result, None);
}
#[test]
fn test_find_map_vs_filter_map_next() {
// find_map is equivalent to filter_map(...).next()
let strings = ["a", "2", "b", "3"];
let via_find_map = strings.iter().find_map(|s| s.parse::<i32>().ok());
let via_filter_map = strings.iter().filter_map(|s| s.parse::<i32>().ok()).next();
assert_eq!(via_find_map, via_filter_map);
assert_eq!(via_find_map, Some(2));
}
}
Deep Comparison
OCaml vs Rust: Transform-and-Find with find_map()
Side-by-Side Code
OCaml
let find_map f lst =
let rec aux = function
| [] -> None
| x :: xs -> (match f x with Some _ as r -> r | None -> aux xs)
in aux lst
let () =
let strings = ["hello"; "42"; "world"; "17"; "foo"] in
let first_int = find_map int_of_string_opt strings in
Printf.printf "First int: %s\n"
(match first_int with Some n -> string_of_int n | None -> "None")
Rust (idiomatic)
pub fn first_int(strings: &[&str]) -> Option<i32> {
strings.iter().find_map(|s| s.parse::<i32>().ok())
}
Rust (functional/recursive — mirrors OCaml)
pub fn find_map_rec<T, B, F>(list: &[T], f: F) -> Option<B>
where
F: Fn(&T) -> Option<B>,
{
match list {
[] => None,
[head, tail @ ..] => f(head).or_else(|| find_map_rec(tail, f)),
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| find_map | val find_map : ('a -> 'b option) -> 'a list -> 'b option | fn find_map<B, F: Fn(&T) -> Option<B>>(&[T], F) -> Option<B> |
| List type | 'a list | &[T] (slice) |
| Optional value | 'a option | Option<B> |
| Parse int | int_of_string_opt : string -> int option | str::parse::<i32>().ok() |
Key Insights
List.find_map in 4.10; Rust's Iterator::find_map has been stable since 1.30 — it's the idiomatic standard.Option**: The convention is identical — None means "skip", Some(v) means "stop and return v". Both languages encode early termination purely through the return type.Some is found, remaining elements are never processed. Rust's iterator chain makes this explicit and zero-cost.find_map vs filter_map().next()**: In Rust, iter.find_map(f) is exactly iter.filter_map(f).next() but communicates intent more directly — you're searching, not building a collection.[head, tail @ ..]) with .or_else() replacing the match on the recursive call, keeping the functional structure intact.When to Use Each Style
**Use idiomatic Rust (find_map)** when scanning an iterator for the first successfully-transformed element — parsing, config lookup, file extension matching.
Use recursive Rust when teaching the OCaml parallel explicitly or when working with custom recursive data structures where the iterator API doesn't apply directly.
Exercises
find_valid_config(sources: &[&str]) -> Option<Config> using find_map to try each source and return the first successfully parsed config.first_match_group<'a>(patterns: &[Regex], text: &'a str) -> Option<&'a str> using find_map to return the first regex match.resolve_path(dirs: &[&Path], filename: &str) -> Option<PathBuf> using find_map that searches directories for the file.