Skip to content

Commit 577d29c

Browse files
committed
Auto merge of #49098 - matklad:find_map, r=KodrAus
Add Iterator::find_map I'd like to propose to add `find_map` method to the `Iterator`: an occasionally useful utility, which relates to `filter_map` in the same way that `find` relates to `filter`. `find_map` takes an `Option`-returning function, applies it to the elements of the iterator, and returns the first non-`None` result. In other words, `find_map(f) == filter_map(f).next()`. Why do we want to add a function to the `Iterator`, which can be trivially expressed as a combination of existing ones? Observe that `find(f) == filter(f).next()`, so, by the same logic, `find` itself is unnecessary! The more positive argument is that desugaring of `find[_map]` in terms of `filter[_map]().next()` is not super obvious, because the `filter` operation reads as if it is applies to the whole collection, although in reality we are interested only in the first element. That is, the jump from "I need a **single** result" to "let's use a function which maps **many** values to **many** values" is a non-trivial speed-bump, and causes friction when reading and writing code. Does the need for `find_map` arise in practice? Yes! * Anecdotally, I've more than once searched the docs for the function with `[T] -> (T -> Option<U>) -> Option<U>` signature. * The direct cause for this PR was [this](https://github.com/rust-lang/cargo/pull/5187/files/1291c50e86ed4b31db0c76de03a47a5d0074bbd7#r174934173) discussion in Cargo, which boils down to "there's some pattern that we try to express here, but current approaches looks non-pretty" (and the pattern is `filter_map` * There are several `filter_map().next` combos in Cargo: [[1]](https://github.com/rust-lang/cargo/blob/545a4a2c930916cc9c3dc1716fb7a33299e4062b/src/cargo/ops/cargo_new.rs#L585), [[2]](https://github.com/rust-lang/cargo/blob/545a4a2c930916cc9c3dc1716fb7a33299e4062b/src/cargo/core/resolver/mod.rs#L1130), [[3]](https://github.com/rust-lang/cargo/blob/545a4a2c930916cc9c3dc1716fb7a33299e4062b/src/cargo/ops/cargo_rustc/mod.rs#L1086). * I've also needed similar functionality in `Kotlin` several times. There, it is expressed as `mapNotNull {}.firstOrNull`, as can be seen [here](https://github.com/intellij-rust/intellij-rust/blob/ee8bdb4e073fd07142fc6e1853ca288c57495e69/src/main/kotlin/org/rust/cargo/project/model/impl/CargoProjectImpl.kt#L154), [here](https://github.com/intellij-rust/intellij-rust/blob/ee8bdb4e073fd07142fc6e1853ca288c57495e69/src/main/kotlin/org/rust/lang/core/resolve/ImplLookup.kt#L444) [here](https://github.com/intellij-rust/intellij-rust/blob/ee8bdb4e073fd07142fc6e1853ca288c57495e69/src/main/kotlin/org/rust/ide/inspections/RsLint.kt#L38) and [here](https://github.com/intellij-rust/intellij-rust/blob/ee8bdb4e073fd07142fc6e1853ca288c57495e69/src/main/kotlin/org/rust/cargo/toolchain/RustToolchain.kt#L74) (and maybe in some other cases as well) Note that it is definitely not among the most popular functions (it definitely is less popular than `find`), but, for example it (in case of Cargo) seems to be more popular than `rposition` (1 occurrence), `step_by` (zero occurrences) and `nth` (three occurrences as `nth(0)` which probably should be replaced with `next`). Do we necessary need this function in `std`? Could we move it to itertools? That is possible, but observe that `filter`, `filter_map`, `find` and `find_map` together really form a complete table: ||| |-------|---------| | filter| find| |filter_map|find_map| It would be somewhat unsatisfying to have one quarter of this table live elsewhere :) Also, if `Itertools` adds an `find_map` method, it would be more difficult to move it to std due to name collision. Hm, at this point I've searched for `filter_map` the umpteenth time, and, strangely, this time I do find this RFC: rust-lang/rfcs#1801. I guess this could be an implementation though? :) To sum up: Pro: - complete the symmetry with existing method - codify a somewhat common non-obvious pattern Contra: - niche use case - we can, and do, live without it
2 parents 5ee891c + 591dd5d commit 577d29c

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

src/libcore/iter/iterator.rs

+32
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,38 @@ pub trait Iterator {
17451745
}).break_value()
17461746
}
17471747

1748+
/// Applies function to the elements of iterator and returns
1749+
/// the first non-none result.
1750+
///
1751+
/// `iter.find_map(f)` is equivalent to `iter.filter_map(f).next()`.
1752+
///
1753+
///
1754+
/// # Examples
1755+
///
1756+
/// ```
1757+
/// #![feature(iterator_find_map)]
1758+
/// let a = ["lol", "NaN", "2", "5"];
1759+
///
1760+
/// let mut first_number = a.iter().find_map(|s| s.parse().ok());
1761+
///
1762+
/// assert_eq!(first_number, Some(2));
1763+
/// ```
1764+
#[inline]
1765+
#[unstable(feature = "iterator_find_map",
1766+
reason = "unstable new API",
1767+
issue = "49602")]
1768+
fn find_map<B, F>(&mut self, mut f: F) -> Option<B> where
1769+
Self: Sized,
1770+
F: FnMut(Self::Item) -> Option<B>,
1771+
{
1772+
self.try_for_each(move |x| {
1773+
match f(x) {
1774+
Some(x) => LoopState::Break(x),
1775+
None => LoopState::Continue(()),
1776+
}
1777+
}).break_value()
1778+
}
1779+
17481780
/// Searches for an element in an iterator, returning its index.
17491781
///
17501782
/// `position()` takes a closure that returns `true` or `false`. It applies

src/libcore/tests/iter.rs

+27
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,33 @@ fn test_find() {
11461146
assert!(v.iter().find(|&&x| x % 12 == 0).is_none());
11471147
}
11481148

1149+
#[test]
1150+
fn test_find_map() {
1151+
let xs: &[isize] = &[];
1152+
assert_eq!(xs.iter().find_map(half_if_even), None);
1153+
let xs: &[isize] = &[3, 5];
1154+
assert_eq!(xs.iter().find_map(half_if_even), None);
1155+
let xs: &[isize] = &[4, 5];
1156+
assert_eq!(xs.iter().find_map(half_if_even), Some(2));
1157+
let xs: &[isize] = &[3, 6];
1158+
assert_eq!(xs.iter().find_map(half_if_even), Some(3));
1159+
1160+
let xs: &[isize] = &[1, 2, 3, 4, 5, 6, 7];
1161+
let mut iter = xs.iter();
1162+
assert_eq!(iter.find_map(half_if_even), Some(1));
1163+
assert_eq!(iter.find_map(half_if_even), Some(2));
1164+
assert_eq!(iter.find_map(half_if_even), Some(3));
1165+
assert_eq!(iter.next(), Some(&7));
1166+
1167+
fn half_if_even(x: &isize) -> Option<isize> {
1168+
if x % 2 == 0 {
1169+
Some(x / 2)
1170+
} else {
1171+
None
1172+
}
1173+
}
1174+
}
1175+
11491176
#[test]
11501177
fn test_position() {
11511178
let v = &[1, 3, 9, 27, 103, 14, 11];

src/libcore/tests/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#![feature(atomic_nand)]
4949
#![feature(reverse_bits)]
5050
#![feature(inclusive_range_fields)]
51+
#![feature(iterator_find_map)]
5152

5253
extern crate core;
5354
extern crate test;

0 commit comments

Comments
 (0)