SE-0207: Add a containsOnly algorithm to Sequence

I think your take is belied by the number of posters in this very thread who report initially having the wrong interpretation.

The trailing closure version of all(satisfy:) is grades.all { isAtLeastBPlus($0) } which amplifies the risk of misinterpretation as a filter-like operation IMO. This suggests it at least would need to go in the base name.

4 Likes

Maybe, but I think the mindset of participants in a forum looking at a name for potential sources of confusion is different from that of people (maybe even the same people) reading the method in actual use.

1 Like

@Ben_Cohen Is this correct? This is surprising to me (I would expect false) but reviewing the proposal it doesn't explicitly specify how empty sequences are handled. The equivalent code example (written in terms of negated contains) does suggest it would return true here though.

This is why I brought up the containsOnly(elementsEqualTo:) and containsOnly(elementsWhere:) as more specific alternatives, which better eliminate the ambiguity. Even so, I don't think they pull their weight.

all was discussed in the thread that brought this proposal forward in the first place, and then community feedback saw it changed it to containsOnly.

I still think containsOnly is superior, as I consider it in the same family as contains and should share a similar base name. For me, the base name preference is not driven by the fact the algorithm can incidentally be defined in terms of contains.

I hadn't considered this before. I think that's a pretty strong argument against containsOnly.

fwiw, the trailing closure version is the one that gives me concern about reading as an alias of filter. If "satisfy" is visible at the call site, I don't think there's a significant risk of that mistake.

2 Likes

For reference, here is how Ruby handles empty sequences:

[].all? { |x| x == 1 }  # => true

It is, exactly, !contains(where: !predicate).

It only returns false if any element does not satisfy the test (which sounds pretty solid to me).

Hmm, good point on the confusion with filtering. But I’d think the type system helps avoid that misconception while writing code, and the differences in typical uses of booleans vs. arrays (if vs. for) when reading.

I think the biggest place where confusion could occur (without the compiler catching it) is when immediately converting the result into something untyped, like print(...) or JSON serialisation.

You're right, I wasn't explicit about it in the proposal, but obliquely referenced it by describing containsOnly as !contains(!condition), which does return true for empty sequences. AFAIK this is the convention in most languages (including Python, Ruby, Rust).

I'll update the proposal.

2 Likes

I believe the concern is that spellings like the following could be interpreted as a filter:

studentBody.all{ $0.grade >= bPlus }

If we use “allMatch” or “allSatisfy” (as well as “allEqual”) then there is no confusion.

I don’t really see the existence of “contains” and “contains(where:)” as having much influence on how we name this. Actually, if anything, there is an argument that we should *not* spell this with “contains”:

  1. If people thought about the method in question as a variant of “contains”, then they would just write the double-negated version and it would be easily understood.

  2. But the double-negated version is not easily understood, and people have to think harder about it to make sure they get it right.

  3. Therefore, people do not generally think of the method in question as a variant of “contains”.

If someone is looking for this method, I posit they are going to look for something with the word “all” in it. After all, they are trying to find out if all elements in a sequence have some property. They are not asking nor thinking about containment.

4 Likes

In my opinion, the word equal should not be used because the function is available for all sequences and not only for sequences whose elements are equatable.

+1

1 Like

FWIW, any/all/none is one of the handful of extensions I blindly copy into every new bit of Swift code I play with. I will likely continue to do so if named containsOnly, because I find all so much better.

+1 to naming this all(equal:) and all(satisfy:), or something similar.

6 Likes

If you want to remove the presumption of filter, you can consider whereAll(satisfy:)/ whereAll(equal:) or whereAllElements(satisfy:)/whereAllElements(equal:). You could sub do for where to make it more of a question.

It's a bit harder but not impossible to coerce the sentiment into the preferred is-prefix, such as isUniformly(equalTo:)/isUniformly(satisfiedBy:). Using is clarifies the Boolean nature of the call and adheres to the API guideline:

Uses of Boolean methods and properties should read as assertions about the receiver when the use is nonmutating, e.g. x.isEmpty, line1.intersects(line2).

I agree that "only" can be misread as "only one".

If "all" looks like filtering, maybe it needs a boolean prefix:

grades.containsAll(4)
grades.containsAll(where: isAtLeastBPlus)
grades.isAll(4)
grades.isAll(where: isAtLeastBPlus)

Or you could try "exclusively" or "strictly" instead of "only":

grades.containsExclusively(4)
grades.containsExclusively(where: isAtLeastBPlus)
grades.isExclusively(4)
grades.isExclusively(where: isAtLeastBPlus)

I wasn't going to say it, but I was thinking the same thing.

1 Like

Agreed. Boolean contexts are almost overwhelmingly obvious from the use site, so the risk of this confusion is very low. containsOnly doesn't have that advantage.

To me, using named predicate with trailing syntax like this is a code smell that linters should care about. IMHO, trailing syntax is for inline closures, where this confusion does not arise.

Generally, the possibility to omit the argument labels, which are there to increase the clarity at use site, when using trailing closure syntax isn’t a valid reason for moving them into core method names. Consequently applied it leads back to the Obj-C/Cocoa naming, not the Swift naming guidelines.

Also I don’t understand the concerns about potential mis-interpretations as filtering, in a strongly typed language. (I would grant them if compiler couldn’t smack you on the head for confusing Bool with [T].)

It seems to me that when way back when the contains was added, the name made sense in isolation. But now that we consider adding its dual, we have exposed that name as inadequate. What exactly is the cost trade-off for contorting ourselves with containsOnly and expanding the Swift Galapagos, versus embracing established programming terms of art and renaming contains to any as we introduce all?

What exactly is the prohibitive(?!?) cost of renaming a single method by simply deprecating the old name, making it forward to the implementation with new name, and providing automated migration fixit? Why do we have to live with all the past mistakes forever?!

7 Likes

I would prefer the plain any/all/none, but if someone feels like a prefix is required then would areAny/areAll/areNone make more sense and read better for plural variable names than the is variants, which seem less clear as to whether they apply to the Sequence or its elements?

In the pitch thread, Brent pointed out that many of the predicate functions passed in as arguments will start with “is”, such as “isEmpty” or “isWhitespace” or “isPassingGrade”. If the base name of the method began with “is” or “are” then these arguments would make call-sites read poorly.

I think we’re on the right track with either allMatch(_:) or allSatisfy(_:) for the version taking a predicate, and allEqual(_:) taking an equatable element.

Sure, that's one downside. is- predicates read fine with any/all/none though, which would be my preference and avoids having separate names. I don't find the extra words that clarifying (they rely on the somewhat subtle difference between match and matching) and I think any/all/none are common enough in other languages to qualify for a precedent/terms-of-art exception to the “avoiding ambiguity” rule. I believe the boolean nature will generally be obvious from context.