rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.97k stars 12.53k forks source link

Proposal: add `reject` function to std::iter::Iterator #65669

Closed olegnn closed 3 years ago

olegnn commented 4 years ago

This method will act as a filter with a complement of the provided predicate.

If function defined as is_something(&value) -> bool {...} (which is a common way) and we'd like to pick opposite values, now ther's a must to create unnecessary closure .filter(|value| !is_something(value)), but instead we could simply write .reject(is_something).

It's also very useful when a condition is a complex bool expression


struct Letter {
    date: DateTime<Utc>,
    text: String,
    sender: String,
    receiver: String,
}

.filter(|letter| {
    letter.sender != letter.receiver
        && letter.date >= date
        && !sender_blacklist
            .into_iter()
            .any(|sender| letter.sender == *sender)
        && !is_spam(letter)
})

.reject(|letter| {
    letter.sender == letter.receiver
        || letter.date < date
        || sender_blacklist
            .into_iter()
            .any(|sender| letter.sender == *sender)
        || is_spam(letter)
})

The first function reads as take if is not and if (greater or equal to) and is not and is not which is much less clear than drop if is or less than or is or is

Moreover, there's a partition function which acts as a combination of filter and reject but we don't actually have reject.

the8472 commented 4 years ago

That seems like a spotfix for just one function that takes predicates, a more general solution would be adding a trait for predicate functions that provides .negate() on function pointers.

Prior art: the java standard library function interface for predicates

olegnn commented 4 years ago

In my opinion, .negate() function will only make code less readable because to apply it in place we should write

let odds = iter.filter((|&a| a % 2 == 0).negate()) // which is much less readable than
let odds = iter.filter(|&a| a % 2 != 0) // and
let odds = iter.filter(|&a| !(a % 2 == 0))

If apply to defined function, it still will lose to |v| !f(v)

let odds = iter.filter(is_even.negate());
let odds = iter.filter(|v| !is_even(v));

I didn't write any code on Java for many many years and I don't know anything about its current status, but as far as I know most of modern JVM-compatible languages have the same reject function:

Kotlin filterNot Scala filterNot Clojure remove

Iterator trait contains 11 functions which take predicate - all, any, filter, skip_while, take_while, partition_in_place, partition, is_partitioned , find, position, rposition.

I think it's obvious why any, find, partition_in_place, partition, is_partitioned,position, rposition, skip_while/ take_while don't suggest to have opposite versions.

So we have two functions all and filter. all may have opposite function none which checks that no elements match given predicate and filter may have reject function as described above.

In many languages it's called filterNot but I'd prefer reject because filter_not looks a bit bulky.

olegnn commented 4 years ago

reject seems to be the result of languages convenience evolution - for example, in Erlang there's no such function but its modern version Elixir has it https://hexdocs.pm/elixir/Stream.html#reject/2.

the8472 commented 4 years ago

I think it's obvious why any, find [... ]don't suggest to have opposite versions.

While all could be negated by a universal quanitification none there could also be a negated existential quantification as the opposite for any, it would check that there exists at least one element where the predicate doesn't hold true.

Similarly one could find the first element that does not match a predicate, if any.

At least logically those are valid constructs, perhaps they don't have many uses in practice, the point is that the concern of negation is one of the predicate, not of the iterator.

If apply to defined function, it still will lose to |v| !f(v)

let odds = iter.filter(is_even.negate());
let odds = iter.filter(|v| !is_even(v));

Well, perhaps it could be implemented as std::ops::Not? Then you could either

.filter(is_even.not())
.filter(not(is_even))
.filter(!is_even)

This could be further extended to other boolean operators, then has the option to either use operators or function calls in prefix or infix position to combine predicates.

.filter(divisible_by_three.or(divisible_by_five))
.filter(or(divisible_by_three, divisible_by_five))
.filter(divisible_by_three || divisible_by_five)

I think this also has the advantage that these types can be named and could be specialized on.

scottmcm commented 4 years ago

If anyone wants this, I suggest they just make a PR. This is a small thing that just needs some libs teams eyes on it to see what they think. (I'm personally not a fan of this, and would rather invert whatever's in the closure, but it's not up to me.)

dogweather commented 4 years ago

I'll have a go at writing a PR. I'm a fan of reject from Elixir and Ruby. IMO it helps create more expressive code without mental overhead. I find it to be a logical complement to filter.

rylev commented 3 years ago

Going to close this. If a PR is opened then that can be used for tracking.

Cldfire commented 2 years ago

I opened a PR for this: https://github.com/rust-lang/rust/pull/93708