Should we add `Iterator::none`?

I'm used to having Enumerable#none? from Ruby and so I was a bit surprised when I noticed that Iterator::none isn't defined.

It is pretty easy to add via an extension trait but I thinking if it makes sense to be in std::iter::Iterator?

pub trait NoneIteratorExtension: Iterator {
    fn none<F>(self, predicate: F) -> bool
    where
        F: Fn(Self::Item) -> bool;
}

impl<T> NoneIteratorExtension for T
where
    T: Iterator,
{
    fn none<F>(mut self, predicate: F) -> bool
    where
        F: Fn(Self::Item) -> bool,
    {
        self.all(|item| !predicate(item))
    }
}
3 Likes

This would be the same as !Iterator::any, which I find to be as clear as Iterator::none

8 Likes

True but personally don’t always think !any is clearer. And sometimes you have a long chain of iterator methods so adding ! way at the start is confusing.

1 Like

Instead of adding ! at the start of a long chain, you can use the Not trait and add .not() after .any(f).

2 Likes

I personally think that any(f).not() is sufficient.

Also, in terms of pure vocabulary, when reading none(), I expect to somehow see an empty iterator.

18 Likes

I didn't know about .not(). That does make things a bit more ergonomic I guess. I still don't think the readability is as good though.

I also don't think that just because you can combine two existing methods doesn't mean we shouldn't add this as a convenience. We have .find_map(f) which is just .filter_map(f).next() :man_shrugging:

One could of course also use a local variable. That's very readable and everyone will understand what is going on.

1 Like

Personally I like Ruby's style of offering logical duals consistently. any has none, if has unless. Yes, one is the negation of the other, and there are a variety of ways to write code that does the same thing (negation, importing the Not trait to use .not() [although I think most folks may not be aware this is possible]), but I personally think writing none is clearer (and less likely to do wrong) than the alternatives.

1 Like

Good to know I'm not alone :blush:

1 Like

I don't see Ruby as "consistent" in that sense, because there's no negation of Enumerable#all?.

1 Like

There's Enumerable#none.

if you want to be complete, then you would need something that acts like iter.all(f).not()

iter.any(f) == iter.none(f).not();
iter.none(f) == iter.any(f).not();

iter.all(f) == iter.foo(f).not();
iter.foo(f) == iter.all(f).not();
1 Like

No, this is incorrect. iter.all(f).not() should return true if the iterator may have some elements that pass the test, but not all of the elements pass the test.

iter.all(f)  == iter.foo(f).not()  == iter.none(|x| f(x).not()) == iter.any( |x| f(x).not()).not()
iter.any(f)  == iter.none(f).not() == iter.foo( |x| f(x).not()) == iter.all( |x| f(x).not()).not()
iter.none(f) == iter.any(f).not()  == iter.all( |x| f(x).not()) == iter.foo( |x| f(x).not()).not()
iter.foo(f)  == iter.all(f).not()  == iter.any( |x| f(x).not()) == iter.none(|x| f(x).not()).not()

It's kind of like any_false where the current any is like any_true.

3 Likes

FWIW, one could replace these with their boolean equivalent:

all    ~ and 
any    ~ or
none   ~ nor
foo    ~ nand

Might even make sense to have these, next to sum and product for values.

2 Likes

Just as another data point of prior art, the C++ STL also has std::all_of, std::any_of and std::none_of. I assume that the fourth variant was deemed to rare to deserve its own function.

Just to make it even more clear, here's a little working example showing the difference between any and what I've called any_not: Code on Playground

1 Like

Teeechnically there's more:

all some none boolean name
0 0 0 false contradiction
0 0 1 nor none
0 1 0 not equal some_but_not_all
0 1 1 nand not_all
1 0 0 and all
1 0 1 equal all_or_none
1 1 0 or any / some
1 1 1 true tautology
6 Likes

Alright, here's an updated Playground example covering all the cases. Functions already provided are commented out. Two of the functions have an added Copy bound which would be avoided in a real-world implementation, but which is placed here to simplify the example: Playground example

These implementations are not correct.

    let v = vec![2, 1];
    let r = v.iter().any_not_all(is_odd);
    assert_eq!(r, true); // panics

    let v = vec![1, 2];
    let r = v.iter().all_or_none(is_odd);
    assert_eq!(r, false); // panics

That's because the any and all methods consume items from the iterator, so when one returns early, the other only checks the remainder of the elements.

Normally that would be an argument in favour of inclusion, but… how often does one need all_or_none or any_not_all, really? I don't see any good use case for it. And the others can already be succinctly expressed in terms of !, or if you insist, Borat conditions. They aren't any more expressive, clear or readable than that, and I'd argue they are less: I'd be less confident reading code that freely interchanges those combinators, because I'd have to memorise them all to be sure they perform as they are named (hello, #define FIVE 42). If they aren't there, not only there'd be less to memorise, but the logical structure of the condition would be much more apparent, making it easier to notice how code could be refactored.

Where is @H2CO3 when you need him?

(I know, the real @H2CO3 was the friends we made along the way… Wait, wrong cliché.)

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.