Add another allSatisfy function that has parameter for empty

Introduction

When you use allSatisfy function in Array, Dictionary...
You have to check empty.
Because if Array, Dictionary are empty, results of allSatisfy are true.

let numbers = [Int]()
let numbers2 = [1,2,3,4,5]

func isGreaterThanThree(_ number: Int) -> Bool {
     return number > 3
}

let numbersAreGreaterThanThree = numbers.allSatisfy(isGreaterThanThree) // true
let numbers2AreGreaterThanThree = numbers2.allSatisfy(isGreaterThanThree) // false

// So we must check empty.
func isNumbersAreGreaterThanThree() -> Bool {
     guard numbers.isEmpty == false else {
          return false
     }

     numbers.allSatisfy(isGreaterThanThree)
}

Motivation

When you use reduce function, you can define initial result.
So I think that if we can define empty result, we don't have to check empty.

Proposed solution

func allSatisfy(_ ifEmpty: Bool, _ predicate: (Element) throws -> Bool) rethrows -> Bool {
     guard self.isEmpty == false else {
          return ifEmpty
     }
     
     return self.allSatisfiy(predicate)
}

let numbersAreGreaterThanThree = numbers.allSatisfy(isGreaterThanThree) // false

It makes help people to reduce bolierpate code for empty check.

Do you have some more concrete example?

allSatisfy is just a universal qualifier from first-order logic, which evaluates to true for empty collection.

I don't immediately see what kind of logic needs this special treatment. And treating empty collection the same way you treat collection with non-satisfying element could easily be harmful.

15 Likes

@Lantua
Description of retun value written in document like that.

Return Value
true if the sequence contains only elements that satisfy predicate; otherwise, false.

I can't see anywhere that allSatisfy evaluates to true for empty collection in document.
I think empty collection can't satify predicate. So result of allSatisfy with empty collection have to false.
But, allSatisfy has already release. So I suggest make one more allSatisfy function that has parameter for empty.

There is more detail example

// Async example
var data = [String: Data]()
var requestURLS = [String: URL]() // requestURL must bigger than 0

requestURLS.forEach { keyValue in
     // async function
     func request(keyValue.value, completion: { [weak self] data in
            self?.data[keyValue.key] = data
            self?.jobFinish
     }
}

func jobFinish() {
      guard self.data.isEmpty == false else {
           print("job is not finished")
           return
      }

      if self.data.allSatisfy { self.requestURLS.allKeys.contains($0.key) } {
            print("job is finished")
       } else {
             print("job is not finished")
      }
}

And I edited title to explain my idea more correctly

The collection does not contain any element which fails the predicate. Therefore the collection contains only elements which satisfy the predicate.

There are no counterexamples. You cannot find an element of the collection that doesn’t satisfy the predicate. Every element of the collection satisfies the predicate.

5 Likes

Technically true, but the logic is a little contorted. It would be reasonable and more direct to say, based on the documentation, that the case of the empty collection has undefined behaviour.

The problem is the language used in Apple’s documentation implies the precondition that the collection contains some elements. It’s not unreasonable for a native English speaker to be confused given the ambiguous wording, let-alone the majority of the planet’s population that aren’t native English speakers.

Clearly the behaviour has been decided, so the only practical course left is to improve the phrasing of the method’s documentation. Whether it’s also useful to introduce a variant or successor with definable behaviour for an empty collection… I’m not sure. It seems like the problem is more with the confusion, than the need to write if not collection.isEmpty and … first.

The related method contains(where:) doesn’t return true if the collection is empty. That’s another source of potential confusion, since one could expect that the logical ‘any’ and ‘all’ functions would return the same value on an empty collection, being a ‘special case’.

(Yes, I know that one can rationalise the current behaviour also - that’s not the point; the point is that there’s room for confusion)

5 Likes

While I agree that there is room for improvement in the documentation regarding phrasing (maybe adding a few simple code examples too?), I also think that the current behavior makes sense and I'm not sure if an overload to specify a default for empty collection is really needed. I do, however have no objections to having it because even though I have never come across this need myself I do see the use of it.

3 Likes

This has come up before. The current behavior is unequivocally the correct one. Here’s why.

10 Likes

I understand that this can be confusing to people not accustomed to mathematical thinking, but the current behaviour agrees with how mathematics and logic think about empty collections (it's a so called "vacuous implication", forall x. is_in_list(x) -> predicate(x)).

6 Likes

What others have said is right; the current behavior is unambiguously the correct behavior.

I think the best improvement available here is to simply add a note to the documentation that if the collection is empty, the result is always true.

12 Likes

I'd rather have NonEmptyCollection for this sort of thing.

2 Likes

I don't see how having NonEmptyCollection changes anything; we'd still have "normal" collections, and we would still have allSatisfy defined on them.

Maybe it's just my style, but I would have if let foo = NonEmptyArray(array) higher up rather than have code that needs to do contigency handling later if something is empty.

It's the same as

guard !array.isEmpty else {
  continue/break/return
}

right? Any attempt to mutate foo (should it be var) will invalidate non-emptiness anyway.

Functionally, yes. This is a style thing as much as anything, I think. Like @Max_Howell1, I kind of prefer to use precise typing - and coercions or conversions between them as necessary - rather than assumptions about state. It’s technically safer, if nothing else - I can forget (or screw up) a guard statement, but I can’t compile code if I miss a necessary type conversion.

I personally don’t think the difference is big enough to try changing Swift’s established patterns on this, but I can understand how others can be a bit uncomfortable with it.

I also think there’s better ways to handle this, ultimately - e.g. add constraints to methods and have the compiler infer state from leading statements & conditionals. e.g. allSatisfy could require that the collection not be empty, but Swift would understand that this is guaranteed by a correct guard statement (or similar control flow statement or preceding constraint), and everything would be fine. (minding that allSatisfy isn’t a great example since it’s far from unequivocal that it shouldn’t handle empty collections, but you get the concept)

1 Like

This is exactly what we don't want. allSatisfy has a complete specification today for any Collection, there's no reason to change that to restrict it to being a partial function on Collection.

15 Likes

It was weird for me at first too, but it’s not a bug. Look at it this way: allSatisfy doesn’t look for 100% matching, but 0% failure. That’s why it returns true for empty sequences.

I think I read some other thread on this, and the logic table on two collections possibly satisfying the method and whether the fusion does is only consistent when an empty sequence returns true.

6 Likes

I think this - and other comments about the mathematical conventions & validity of the current behaviour - continue to miss the point: the documentation could simply be clearer, and it's reasonable to suggest making this behaviour configurable since there clearly are valid use cases for both behaviours. The only question on the latter point is if it's justified vs the existing, alternative approach of explicitly checking isEmpty.

I think that if folks can focus on those two points, this thread will be more productive.

There seem to be two different conversations happening here.

  1. can the documentation be clearer?

  2. (from the OP) can we avoid needing to check for empty separately from calling allSatisfy()?

My opinion is yes to (1) and no to (2). More specifically, it would be incorrect to overload allSatisfy() with a function of different semantics or to assume semantics other than those inherited from logic. It may not seem intuitive to some that zero elements can “all satisfy” a predicate but it’s no more unusual than counting the elements of an array and getting 0 as the answer instead of the count function erroring out or having an overload that allows you to say “count these things IF they number more than 0.” There’s no reason individual code bases couldn’t provide conveniences for themselves such as a function (whatever you call it) like nonEmptyAndAllSatisfy().

[EDIT] In fact, leaving these types of conveniences out of the standard library leaves the strategy for handling the OPs situation open to different solutions depending on your taste; I personally would use a NonEmpty type like mentioned up thread in order to move the predicate that the collection is “not empty” As close as possible to the definition of the value, thus reducing the number of times it must be checked for emptiness.

5 Likes

This piece isn't a SE discussion, it's just a PR against the stdlib, since it's uncontroversial and has neither API nor compatibility implications.

For all the reasons discussed upthread, and especially in @mattpolzin's response, this would not be a desirable change.

There's a third piece, which is "what would a more idiomatic way to write this be?"--that's by far the most interesting of the three, but it's also outside the scope of Evolution and would be a better fit for Using Swift.

1 Like