Logical "and", &&, and mapreduce

I wonder if a function like and(x::Bool, y::Bool) = x && y would be useful.

You can currently do
mapreduce(f, &, x, y), where f takes two arguments and returns Bool.

But and always returns Bool (only has this one method). So you could write a method for mapreduce(f, and, x, y) with a fast exit.

Apparently, any is not always an option. isapprox uses mapreduce instead of any.

A couple of potential problems. First, if you add a method for and that does not return Bool, then this will fail. But you shouldn’t do that. (missing and Symbolics might want to anyway.)

Another potential problem is that all was replaced by mapreduce in isapprox in order for it to work with GPU arrays. The issue (#44893) that led to the change said it was because zip was used. But if it is related to the fact that mapreduce is less predictable with a fast exit, then you lose the advantage. I mean if you know the lengths of x and y, then you know how many times f is called and with what data, as long as there is no fast exit.

You can see that isapprox suffers in performance in losing the fast exit. It was judged that the performance loss was worth the gain in generality, which is probably reasonable.

Off the top of my head, I can’t think of other advantages of and over &.

As the author of that PR: the point was just that more or less every array type implements the mapreduce machinery as the foundation for functions like sum, while many common array types such as GPU arrays don’t support iteration/scalar indexing and hence can’t be used with zip. That’s why using mapreduce made the method more generic. This doesn’t preclude having a short-circuiting mapreduce for array types that do support iteration, someone just has to implement it.

I know mapreduce sometimes replaces the provided reducer with a slightly tweaked one, e.g., mapreduce(f, +, x) dispatching to mapreduce(f, Base._secret_add_only_for_mapreduce, x) or something along those lines. So perhaps rather than adding a new public function and and hoping that people start using it, one could dispatch mapreduce(f, &, x) to a short-circuiting mapreduce(f, Base._secret_and, x) if it is inferred that f(::eltype(x)) returns Bool.

Isn’t all an option?

Option for what?

Edit: I guess you mean using all(f, x) instead of mapreduce(f, &, x). That’s true. The issue is actually with mapreduce(f, &, x, y) as described in the OP, because the equivalent all(((x, y),) -> f(x, y), zip(x, y)) uses zip. Sorry for confusing those in my post above.

But maybe you mean implementing multi-argument methods like all(f, x, y) which are equivalent to mapreduce(f, &, x, y) but short-circuiting by default. That sounds like an interesting proposal to me.

Yes, if that’s possible, it would be preferable to another public function. I have no idea how feasible that would be.

Such a function does not preserve the short circuiting behavior of && correctly. and(and(a,b), and(c,d)) requires b as well as and(c,d) to be evaluated when a is false, while a version written with && does not. In (a && b) && (c && d), only a needs to be evaluated. This is because function arguments need to be fully evaluated before the function can be called.

True, and wouldn’t be semantically equivalent to &&, but I don’t think that was the point; the point was to have a function to specialize on when implementing a short-circuiting method of mapreduce(f, &, xs...). You can’t simply make the method for & short-circuiting because it doesn’t always return a Bool, and mapreduce(f, &&, xs...) is nonsense because && is not a function.

I think it was, at least in part:

The only possible reduction for such an and that preserves short circuiting is one like foldl or foldr, you can’t split up the reduction space into a tree-like reduction. && is inherently serial.

Why not? For each node that executes its branches in sequence, if the first branch returned false you skip the second and return. This can save you a lot of computation on a serial processor where every node is of this type. If you’re doing a GPU-style reduction where all branches are concurrent you won’t save anything of course, but that’s the point: mapreduce can have different methods for different array types, with optimizations like this for the ones where it’s suitable.

Also, in the Base implementation of mapreduce, tree reduction only kicks in for lengths greater than 1024, so there can be big serial chunks to short circuit in the base case.

As far as I understood the OP, the question is only about opportunistic performance optimization, not about wanting a semantic guarantee that only values left (or right) of the first false will be inspected.