Hi everyone, just pitching an idea here - The current implementation of iter().all()
returns true
if the iterator is empty. This makes sense against the behaviour of iter().any()
, which returns false
if the iterator is empty. Here, "any" -> at least one element -> if no elements, false. Along the same lines, "all" -> every single element -> if no elements, ...false? I feel this also makes sense, but we cannot alter the behaviour of iter().all()
at this point in such a breaking manner. Would it make sense to introduce another function that does the exact same thing as iter().all()
but returns false if the iterator is empty? Maybe it can be called all_non_empty()
or something like that.
It doesn't. This is called "vacuous truth". If our job is to satisfy a set of conditions, and there are no conditions, then we have done our job. Hence the answer is true
.
This is related to the empty product being 1. If you consider true
=1, false
=0, then all()
is the product. iter::empty().all(|x| x) == true
corresponds to x.pow(0) == 1
.
Do you have a use case in mind for this? It's a fairly simple thing to construct if you need it: (you could do so via peekable
, or via any
and De Morgan's laws, or very explicitly as:
fn all_or_empty<Iter, F>(iter: &mut Iter, mut f: F) -> bool
where
Iter: Iterator + Sized,
F: FnMut(Iter::Item) -> bool,
{
(match iter.next() {
None => false,
Some(item) => f(item),
}) && iter.all(f)
}
In general, though, as @tczajka pointed out, it's rarely algorithmically needed - usually, if you're using all
or any
, you want all
to return true if the iterator is empty, and any
to return false if the iterator is empty.
Another argument is that by de Morgan,
forall x: p = not exists x: not p
And when x is the empty set, this identity reduces to
lhs = not rhs = not false = true.
Of course, this also matches the most common real-world use cases: if the user can filter some data such that all enabled filters are ANDed together, then itβs surely the case that if no filters are set, all the data should be displayed.
Thank you all for the info, my predicate logic was faulty cheers
Because I found it interesting, another way to write this:
fn all_non_empty<Iter: Iterator>(mut iter: Iter, mut f: impl FnMut(Iter::Item) -> bool) -> bool {
iter.next().map_or(false, &mut f) && iter.all(f)
}
Not that open-coding Option::map_or
is a particularly bad thing to do, but it does make the function body a little cleaner, as does taking the iterator by-value (which also accepts by-mut-ref).
I keep forgetting about Option::map_or
, because it didn't exist when I first learnt Rust. It's one that I use when Clippy reminds me of it, then forget about after a few days.
And map_or(false, _)
can be replaced with its special case is_some_and(_)
, which might read better in this case:
iter.next().is_some_and(&mut f) && iter.all(f)
The above posts cover why "every single element -> if no elements, ...false?" is considered incorrect in standard boolean logic, but it's also worth at least a little argument that "standard boolean logic" is how we want our programs to behave.
Consider the following:
let numbers = vec![Ok(1), Ok(2), Err("???"), Ok(4)];
if numbers.iter().any(|n| n.is_err()) {
println!("Invalid number found, skipping expensive_op");
return;
}
println!("All numbers valid, doing expensive_op");
expensive_op(numbers.iter().map(|n| n.unwrap()));
and
if numbers.iter().all(|n| n.is_ok()) {
println!("All numbers valid, doing expensive_op");
return expensive_op(numbers.iter().map(|n| n.unwrap()));
}
println!("Invalid number found, skipping expensive_op");
Today, these are two ways of writing the same thing. But if numbers
was an empty vector, and .all()
were changed to return false
on empty iterators, then the 2nd way of writing this would print "Invalid number found" even though no invalid number was found.
While you do sometimes need special cases for empty sequences, in my experience this sort of code where empty and non-empty sequences "just work" the same way is very typical. I believe .all()
returning false
on empty iterators (or the equivalent in any other programming language) would lead to far more subtle bugs in practice than the current behavior of producing "vacuous truth" like standard logic does.
This example could be rewritten such that one wants expensive_op
to run when `numbers is empty, or that you don't, and the printed text could be valid or invalid for empty either way. The reason we want code and libraries to conform to various logics is so the code can be reasoned about. Someone familiar with boolean logic could figure out what any/all do without even looking at the docs, and is less likely to make a mistake with this edge case.