-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add find_or_last
method to Itertools
trait
#535
Conversation
find_or_last
method to Itertools
trait
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi there! I know of libraries that support an operation like this, so this might be useful for Itertools
, too.
One question: Might it be useful for the user to distinguish between "successful search" and "last element because nothing else succeeded"? (We have a similar (or isn't it?) issue with exactly_one
where a fail could occur because there are either no elements or more than one element.)
Pondering: why is last the right option? Should there also be Would it be fine for people to just do Some thoughts on alternative implementation strategies: My first thought was that this should do the pub fn find_or_last<T>(
mut it: impl Iterator<Item = T>,
mut pred: impl FnMut(&T) -> bool,
) -> Option<T> {
let first = it.next()?;
Some(
it.try_fold(first, |_, x| if pred(&x) { Err(x) } else { Ok(x) })
.map_or_else(|x| x, |x| x),
)
} But the previous accumulator being unused lead to my second thought, namely that if the code doesn't need move access to the accumulator, it probably shouldn't thread it at all. That would look something like this: pub fn find_or_last<T>(
mut it: impl Iterator<Item = T>,
mut pred: impl FnMut(&T) -> bool,
) -> Option<T> {
let mut prev = None;
it.try_for_each(|x| if pred(&x) { Err(x) } else { Ok(prev = Some(x)) })
.err()
.or(prev)
} Which I find pretty elegant, actually. (And avoids the extra |
@scottmcm you might want to look at this reply by steffahn, where he suggests the same approach, with some more refactoring. The result would look like this: fn find_or_last<P>(mut self, mut predicate: P) -> Option<Self::Item>
where Self: Sized,
P: FnMut(&Self::Item) -> bool,
{
let mut prev = None;
self.find_map(|x| if predicate(&x) { Some(x) } else { prev = Some(x); None })
.or(prev)
} if I am not mistaken. |
About the option of using But About why I picked the last value: In my use-case it really doesn't matter which value is returned, I just intuitively thought taking the last value would be the most efficient. But, as it turns out, getting the last value requires an accumulator, so it is probably less efficient than taking the first element. |
using accumulator variable and find_map
I suppose a fn find_or_first<P>(mut self, mut predicate: P) -> Option<Self::Item>
where Self: Sized,
P: FnMut(&Self::Item) -> bool,
{
let first = self.next()?;
Some(if predicate(&first) {
first
} else {
self.find(|x| predicate(&x)).unwrap_or(first)
})
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies if this review is too brief, I do not have much time at the moment.
As far as I can see, the implementations look ok, although I preferred the try_fold
approach for some reason... Cannot really substantiate this feeling. Maybe it would lead to similar implementations for find_or_last
and find_or_first
? Anyways, my feeling should not be a blocker.
My remark regarding the return type stands: Maybe some users want to differentiate between "nothing found because iterator empty" and "nothing found, so the last (resp. first) element was returned instead". On the other hand, we could include your change and wait for actual complaints from users.
Note that custom iterators can't implement |
@phimuemue yes, the idea is still good, and I don't see how it would hurt anything, except maybe requiring something like an enum FindOrDefaultResult<T> {
Found(T),
NotFound(T),
Empty,
}
impl<T> FindOrDefaultResult<T> {
pub fn into_inner(self) -> Option<T> {
match self {
Self::Found(x) |
Self::NotFound(x) => Some(x),
Self::Empty => None,
}
}
} Theoretically this could even lead to more kinds of enum DefaultSelector {
First,
Last,
AtPosition(usize),
Random(Box<dyn rand::RngCore>),
...
} But it is probably better to wait for someone who actually needs something like this to make a proposal, rather than blindly implementing something. |
I am unsure now, should I implement the |
I'd leave it as it is and wait for @jswrenn 's opinion. Tbh, I second your idea of not blindly implementing abstractions that may not even be needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to merge this as-is. We can revisit improvements in the future, as needed.
bors r+
Build succeeded: |
Playground
None
if iterator is emptySome(element)
when element matches the predicateSome(last_element)
when no element matches the predicateRelated discussion on the rust user forum