Adding boolean ops to predicate functions in std

Assuming type Predicate<T> = FnMut(&T) -> bool would the following be a useful addition to the standard library and would it require an RFC?

impl<T> Predicate<T> for bool

impl<T> BitOr/BitAnd<Predicate<T>> for Predicate<T> {
   type Output = Self
}

impl<T> Not for Predicate<T> {
  type Output = Self
}

This would enable the following:

struct Foo {}
impl Foo {
   fn conditional_constructor(p: Predicate<Self>) -> Option<Self>
   fn is_red(&self) -> bool 
   fn is_blue(&self) -> bool
}

fn main() {
  std::iter::once(Foo::conditional_constructor(true)).filter(!(Foo::is_red | Foo::is_blue));
}

I.e. there would be a canonical shorthand for always-true or always-false predicates which are non-closure types. This may come in handy for specialization, saves a few keystrokes and may make it more acceptable to not provide unconditional variants of functions that normally take a predicate. And it would provide a way to compose function references the same way one can compose their results in boolean expressions. The ability to negate predicates would also reduce the need to add functions in pairs such as reject/retain

One wart there is that it would use | instead of || since the latter does not support overloading.

prior discussions and attempts:

That's an interesting API, but probably too niche and magical for the standard library. When I needed this for a DSL, I've used essentially a struct Predicate<F: FnMut(&T) -> bool>(F) wrapper and impled bit ops for it. (source)

Another wrinkle here is that at the moment you won't actually be able to spell the type Output bit without dynamic dispatch. The unboxed output would be a closure type, and those are unnameable. type Output = Box<dyn FnMut(&T) -> bool> would work, but it imposes allocation where it, strictly speaking, not necessary.

1 Like

I think it could be implemented as a named struct carrying the two input predicates, if the type_alias_impl_trait feature were stable the associated type could also be specified via impl Trait

Can you elaborate why this is a problem? |_| true is a ZST, and inlining/folding ought to remove all conditionals involved...

In fact, passing true could even be worse, as it's not a ZST. And because it's a bool, it wouldn't necessarily fold away either, since it can't know from the type whether it's true or false.

3 Likes

It's more of a visual clutter thing, I suspect people may balk at requiring users to pass |_| true to methods and thus, in absence of overloading, add a 2nd method without it. Having true and false as canonical values would look cleaner and be less of a papercut when writing a call to such a function.

The impl<T> FnMut(&T) -> bool for bool could be const and #[inline]. And thus fold to self (the boolean) at call sites, it would enjoy the same optimizations while having to instantiate fewer closures (one per type instead of one per call).

1 Like

This can be done as a library, so it's best to keep it out of std until it's proven itself (something like this)

1 Like

It's supposed to work for any method that takes FnMut(&T) -> bool

Yes, mine works with all Fn(&T) -> bool (with the tiny added wrinkle that you need to call into_pred). You can't get around this wrinkle and add operators, Rust's trait system won't allow it. If you don't need to use the operators, you can just pass the closure and not call into_pred. If you want FnMut, just change Predicate::test's signature from &self to &mut self, and Fn to FnMut. Not a big change.

My point stands that this is easily done as a library, so it should be prototyped outside of std and get some usage before being folded in.

The aim is to streamline syntax. If it's an external crate and you need to add .into_pred() would anyone go through the effort of using it? After all you can always use the closure approach instead.

I think it would be better to figure out how the api is designed before worrying about streamlining it. What works for other languages may not work for Rust, so you will have to figure out what api you require. Note: Rust will never add operator for all F: Fn(&T) -> bool, because the plan is to allow user defined implementations for Fn* operators. So you will need to rethink how this will operate anways.

On top of this, Rust likes to keep it's std as minimal as possible. There are some warts here and there, but let's not bloat things too fast. If this is really useful to a large number of people, then it could be added to std, but that's unlikely given that even something like random numbers or serialization are not in std, and they probably have more usecases than predicates.

There are a number of ways to streamline my design, try to figure some out and improve on my design. I really didn't put much thought into it, I largely based it off the Iterator trait, so you can look there for ideas.

2 Likes

Can you elaborate? Considering this is central to the bitops part of what I am proposing.

Also note that your comment says nothing about the other aspect, specifically implementing it for the values of true and false, which only std can do.

Rust will never add the impl

impl<F: FnMut(&T) -> bool, T: ?Sized> BitAnd<Anything> for F { ... }

Because

  1. It's ill-formed, you can't have a free T generic parameter
  2. It would prevent library code from implementing FnMut and BitAnd on their own.

Similarly for all other operators.

You can implement your own traits for external types. See impl Predicate for bool in my playground example

1 Like

To add to @RustyYato's points, Rust is not intended to be a std-library-only language. Crate usage is expected as part of the development process. Rust has "forever" forward-compatibility guarantees for the language and for the std library, which preclude breaking changes, whereas crates can and do use SemVer to enable and manage breaking changes. Lastly, nothing gets in std without first having proved its correctness, its generality, and its usefulness to the community via the external crate route.

2 Likes

That seems like a misrepresentation of reality. I have seen plenty of minor additions being made to libstd without referencing a crate implementing those changes beforehand.

Ah, well, That pretty much puts a knife into it.

After thinking about this a bit more (now that you got me to :slight_smile:), I think you cold do something like this: playground, which honestly, isn't that bad. You'll have to start every predicate with False | or True & , but after that nothing, you can just use functions!

edit: added missing bool: IntoPredicate<_> impl

If you want to develop this further, you can go ahead and take this code. I won't be working on it any more.

That's worse than |_| true, though, because if it's folding to a value not to a constant.

To be concrete, let's use https://doc.rust-lang.org/std/iter/struct.Filter.html.

.filter(|_| true) gives Filter<I, AlwaysTrueClosure>, which is the same size as I, and its .next() method compiles down to just I::next().

.filter(true) (with this hypothetical Fn impl) gives Filter<I, bool>, which is at least one byte larger than I, and where's there's a test in the generated .next() method.

This could be solved by having std::functions::True, but I'd rather type |_| true than a name like that.