Add operators for working with unary predicates

I frequently find myself wishing for more expressivity when using predicate functions, and so I end up defining these operators so I don't have to create cluttered little closures:

func && <Element>(
  lhs: @escaping (Element) -> Bool, rhs: @escaping (Element) -> Bool
) -> (Element) -> Bool {
  return { lhs($0) && rhs($0) }
}

func || <Element>(
  lhs: @escaping (Element) -> Bool, rhs: @escaping (Element) -> Bool
) -> (Element) -> Bool {
  return { lhs($0) || rhs($0) }
}

prefix func ! <Element>(x: @escaping (Element) -> Bool) -> (Element) -> Bool {
  return { !x($0) }
}

These operators combine and modify predicates in predictable ways, and (for me) are much nicer to read than the same thing wrapped in curly braces:

func isEven<T: BinaryInteger>(_ x: T) -> Bool {
  return x % 2 == 0
}

func isMultipleOfThree(_ x: Int) -> Bool {
  return x % 3 == 0
}

var a = 1...10
a.filter(isEven)                         // [2, 4, 6, 8, 10]
a.filter(!isEven)                        // [1, 3, 5, 7, 9]
a.filter(isEven && isMultipleOfThree)    // [6]
a.filter(isEven && !isMultipleOfThree)   // [2, 4, 8, 10]
a.filter(isEven || isMultipleOfThree)    // [2, 3, 4, 6, 8, 9, 10]
a.filter(!(isEven || isMultipleOfThree)) // [1, 5, 7]
16 Likes

Forgot to mention that it's not just curly braces — you need to actually call the predicates inside a closure, which is more extra stuff to look past:

// current
a.filter({ isEven($0) && isMultipleOfThree($0) })
// pitched
a.filter(isEven && isMultipleOfThree)

I like this idea. It will certainly be useful when handling boolean predicates.

However, the thought occurs to me, is it possible to make this more general? As in, given two functions f and g, where the return value of g matches the input to f, can we make something like f(g) or f.g resolve to the closure { f(g($0) } or equivalent?

In other words, can we make function composition “just work”?

2 Likes

A function composition operator would be a more general version of the && operator above. (Update: this is super wrong.) Spelling it just as a . wouldn't work, since that's not an allowed character for operators, but others have gone with • instead.

In general, I think the order of operands with that operator can be confusing (to me it always looks backwards), and I've seen some implementations use >>> and <<< for that reason. I would support some kind of proposal for a function composition operator, but I think the motivation and considerations are different from what I'm discussing here.

2 Likes

I’ve wanted this for a long time, and I’m glad that it’s not just me.

It seems very much a natural way to express things. After all, we write != and ==; this would mean that !(==) is !=, as it should be.

Following on from Xiaodi’s mention of !=, one of the basic boolean operators that Nate’s post does not mention is “xor”, which in Swift is spelled “!=”.

Personally, I think “!=” is a non-intuitive way to spell “xor”, but if that’s what we’ve landed on then should we include it in Nate’s proposal? Then if you want to test whether exactly one of two conditions is met you can write:

a.filter(isEven != isMultipleOfThree)   // xor

As written, Nate’s “!” operator takes a unary predicate, meaning a function that takes one argument. However “==” takes two arguments. We would need another overload of “!” to make your idea work.

In an ideal world, we could declare a version of “!” that takes a function with any number of arguments of any types that returns a Bool, which then produces a function with the exact same signature but the result negated.

I'm not sure that the behavior of that operator reads clearly—it looks like it's comparing whether the two closures are equal, instead of producing their symmetric difference (if that's the right term).

Are these operators as clear for binary predicates? It's funny, I get frustrated by the lack of the ones I listed all the time, but have never wanted them for comparisons. For custom comparisons I always write out the closure.

I don't have a lot to say for or against, but note that the first one can also be written as:

a.filter{ isEven($0) && isMultipleOfThree($0) }

which eliminates the outer parens but retains the clarity of explicitly passing the closure parameter. I'll gladly admit that one person's clarity is another person's boilerplate so I wouldn't be opposed to such predicate combinators if people find them generally useful enough to warrant their addition to the standard library.

2 Likes

I'm a fan. While there might be a concern of readability at the call sight (it's easy to think those functions are Bools at first glance) but it's probably not any less clear than a.filter(isEven) already is so I'd say it's a win.

It slipped my mind part way through reading that you were only proposing operators for unary predicates. I think that's fine and certainly of the highest impact.

To be honest, though, the operator I've had the most occasion to want for these purposes is !. To compare the two scenarios, I think I've wanted that unary operator for binary predicates vastly more often than I've wanted any binary operators for unary predicates.

1 Like

Are you sure @rudkx? 'cos I would guess overloading && will have some implications for the speed of the type checker.

This is my main concern with re-using the operators, and why I'd suggest suggest free functions: not, or, and, the latter two of which could be variadic to allow 3+ predicates.

Other than that, I'm very much in favor of adding these.

1 Like

Oh, also +1 for also adding f • g

3 Likes

Good idea

/cc @xedin as he has been working hard to eliminate free hanging operators from the stdlib and group them under some protocols, and may have something to say about why re-using the same operators can be a bad idea.

A big +1 for me on this.

One day, when we have variadic generics, I'd love to see a full slate of operators that can be applied to functions. I can imagine some really interesting functional patterns being possible by being able to compose functions using the common operators when the signatures and return types match (the generics syntax below is obviously a straw-man, just to get the point across):

func + <Args..., Result: Numeric>(
  f: (Args...) -> Result,
  g: (Args...) -> Result
) -> (Args...) -> Result {
  return { args... in f(...args) + g(...args) }
}

But, until we're able to do something this elaborate, I'm totally supportive of doing it for predicates like the ones above. It would definitely improve the usability of the collection/sequence APIs that involve predicates when multiple conditions are in use.

That's always a great question to ask, and I'm always happy to be pinged on such questions and answer.

In this case the only thing that would concern me would be if there were also variants that took unconstrained generic function arguments.

If you take a look at this you'll see that it is pretty much instantaneous to typecheck, unless you add -DSLOW, in which case it won't compile.

We're able to bottom-out pretty quickly on mismatches like function type vs. concrete type. We're not able to do so for unconstrained generics, and I suspect today even for constrained generics we wouldn't do particularly well here, but I have plans to improve at least some of those cases.

4 Likes

I'm also very much in support of this!

And in general, any operation that one might do on a type B can be transported to an operation on (A) -> B by just applying the operation pointwise, e.g. op(f) = { x in op(f(x)) }.

From that perspective it's possible to think of functions into B as being nearly the same as B's themselves, as we see in the case of predicates.

In the future if -> were a proper type that could be extended, you could make functions just conform to these algebraic protocols, like Numeric, etc... extension (->): Numeric where B: Numeric. Maybe even introduce BooleanAlgebra to unify &&/||/! and get extension (->): BooleanAlgebra where B: BooleanAlgebra out of it too.

edit: oh, and also very much in support of >>> and <<<, but can save that for another day...

3 Likes

Thanks Mark! That gives me hope then that maybe we can re-use ! and &&, and it's just a discussion re clarity or not of overloading them like this.

2 Likes

-1 for me, operators in functional paradigms seem to be the biggest source of confusion, and I definitely don’t like the idea of these overloaded operators being a part of the language. It’s super convenient to know that unless you see an import statement the operators are acting on actual values and not some derivative.

Also, I think the current solution just looks better, especially with a trailing closure.

a.filter { e in isEven(e) && isDivisibleByThree(e) }
a.filter { e in
    return isEven(e)
        && isDivisibleByThree(e)
}