`isNotEmpty` on Array


#9

I know reasoning by analogy is dangerous, but let's discuss the in/not in Python operators, and the contains(_:) Swift method.

Let's say we implement an algorithm read in a book, which contains this sentence:

If the node has already been visited, ...

If our language is Swift, we may write:

if visited.contains(node) {

In Python, however:

if node in visited:

How could both languages provide such different wordings for the same test, considering both have the same constraints: the actual algorithm that tests for inclusion depends on the type of the container. Both in and contains are polymorphic.

This polymorphism is totally assumed in Swift: contains(_:) is declared on Sequence, with more or less private ways for concrete types to provide an efficient implementation. Swift generally wants us to know how methods are dispatched across protocols, classes, structs, etc. This knowledge allows us to write good/fast/maintainable/testable swift code. To put it more mildly, whenever a developer learns about Swift dispatch, this dispatch is clearly visible in the code: contains is a method on the container because containment test depends on the container. The subject is the container because this is how the language works: if visited.contains(node)

However, Python hides this polymorphism behind the __contains__ magic method, documented under a chapter called Emulating container types. This magic method provides the same level of polymorphism which is required for an efficient implementation. But this polymorphism is not visible in the code, because knowledge about polymorphism (and magic methods) is not necessary to use Python, a scripting language. The Python code does not show what is happening, because the language has rather follow natural language. The element is the subject of the containment test, as in English: if node in visited.


Swift requires a more abstract mind than Python.

Swift: if you understand why containement test is a method on the container, then it is likely you'll also understand why there is an independant ! negation operator, and you won't have problem chaining them:

if !visited.contains(node) { ... }

Python: if we provide English-like in operator for containment test, then we'd rather also provide the not in negated operator in order to avoid confusing explanations about expressions, operator precedence, parenthesis, etc:

if node not in visited:

The moral of this story is that Swift has already picked his side. And it is not Python's. Extra apis that help the language get closer to English are not very warmly welcomed, because the language is not grounded on English: it is grounded on the visibility of dispatch. What you see is what is happening.

On a side note, I'm happy I wrote this, because it is an interesting way to answer the "Prefer method and function names that make use sites form grammatical English phrases" mantra of the API Guidelines. Some English sentences will have to be twisted in Swift, because efficient dispatch is more important than the structure of the equivalent English sentence.


EDIT: this is not a hard rule, of course :-) We're discussing about having both isOdd and isEven right now. It looks like those trivial methods will be easily accepted (they're not that trivial, actually). Bool.toggle() had a more contrasted reception. I enjoy reading "little" pitches like isNotEmpty, because they spread the language culture, and influence it in the same time. This is a virtuous feedback loop.

And the list of commonly rejected changes is short.


isNonEmpty
(Tino) #10

I‘d declare that (probably with a different name) on Optional<Collection>.


(Jordan Rose) #11

I'll note that for the negation operators mentioned, all of the "counterparts" you've mentioned besides Ruby's are cases where you'd need parentheses to express the same thing otherwise. Python's predicate functions, for example, don't seem to come in pairs like this; just its operators.


#12

You're right in every bits of your message, Jordan! And thinking about parenthesis was one trigger for the follow-up message. In which I somewhat missed my point in one sentence, because parenthesis are the real culprit (not the confusion I talk about):

Python: if we provide English-like in operator for containment test, then we'd rather also provide the not in negated operator in order to avoid confusing explanations about expressions, operator precedence, parenthesis, etc


(Tim Vermeulen) #13

I think isNotEmpty could be a huge readability improvement in case of long expressions – it often makes more sense logically for the negation part to be at the end of the expression than at the start. For this reason I've seen people write .isEmpty == false or even .isEmpty.not instead.

So I'm in favor of isNotEmpty (and other similarly properties such as UIView.isVisible) but it would be a pain maintainability-wise to implement the negations of all boolean properties in a library's public interface. What about an attribute?

@inverse(isNotEmpty)
var isEmpty: Bool { ... }

This seems to be consistent with the @objc(...) attribute, and it has the additional benefit for consumers of an API that they don't have to guess from the names of two boolean properties that they're each other's inverse.


(Ben Cohen) #15

Thanks for bringing this up. I've been thinking about this recently, especially in the light of a large number of "trivially composable" proposals that have been brought up recently. The problem is mainly the relative weighting of the criteria we've discussed. Readability improvements and minor performance and correctness optimizations are being used as the main criteria, whereas I feel like major performance wins and really hard (but still relatively common) problems to solve correctly are being underweighted.

I think there's a real risk with the current pitches that we end up with a standard library populated with huge quantities of sugar and not enough protein. I feel like we need to refocus on key algorithms that are important and hard to write correctly, rather than minor nice-to-haves which are (sometimes) more common, but also very easily written. A standard library that is missing stable sorting and partitioning, rotation, permutation, and binary search, but does have notEmpty, id, isEven, would be pretty unfortunate.

I see this as guidance when adding something about what to call it, and not guidance as to what to add. That is, once you have decided to add an allSatisfies method, that naming guidance is how you decide to call it that and not all or contains(only:). It isn't an encouragement to add synonyms or trivially composable methods to the standard library. Those can sometimes aid readability – but they harm the usability of the library by bloating it, making it hard to use and navigate.

I think there's an important difference. The motivation for toggle was the need to duplicate long expressions (path.to.something.over.here = !path.to.something.over.here) as well as avoid the risk of typos in that long expression. Neither of these arguments materially apply to !isEmpty over notEmpty.


RFC: making Swift.org a more valuable resource for the Swift community
SE-0225: Adding isEven, isOdd, isMultiple to BinaryInteger
isNonEmpty
(Jon Shier) #16

I think part of the issue is that, after toggle() was approved and the criteria for trivial changes were posted, it became very easy to come up with trivial additions that meet the criteria. Whereas the larger, meatier proposals still suffer from a lack of clear guidelines about how to even be reviewed, as well as a far larger investment in time to come up with and potentially implement in the first place. Personally, I would love having partitioning, rotation, permutation, and binary search in the standard library, in addition to many other things, but I don't have the expertise or time to implement such things. I find it hilariously ironic that Dave's "Embracing Algorithms" talk at WWDC was about algorithms not included in the standard library. But like I said, these sort of meatier changes suffer from a lack of guidance and support and require a full evolution pass for standard collection algorithms. Perhaps these things would be added more easily if they were just approved and just awaiting implementation?


(Steve Canon) #17

I basically agree with everything that Ben wrote.

After a lot of thought, I came down very weakly in favor of isEven and isOdd, so those are roughly my threshold. For me the qualifications for inclusion are three out of four of the following:

  1. popular demand (proxy for better discoverability than alternatives)
  2. better readability at use site than any alternative
  3. correctness hazard if users try to implement it themselves
  4. performance hazard if users try to implement it themselves

isEven and isOdd tick off 1, 2 (maybe), 3 (only isOdd), and 4 (only for bignums, and maybe not even there in the face of compiler heroics). The reason I say "maybe" for 2 is that I'm not totally convinced that we aren't better served by apis for alternating-color list layouts and checkerboards, rather than isEven and isOdd.

isNotEmpty satisfies 1 and maybe 2. It does not satisfy 3 or 4. So (from my perspective), it's just sugar, no protein.


(Ben Cohen) #18

I think there's a middle ground: a menu of algorithms that are approved in principal, but still need proposals to flesh out the details. Even the more straightforward algorithms have nuances in terms of implementation. For example, out-of-place rotation could return either an array or a lazily rotated wrapper, binary search leads to the question of whether you use the type system to guard against searching unsorted collections etc.


RFC: making Swift.org a more valuable resource for the Swift community
(Jon Hull) #19

I do have a metric ton of guard !.isEmpty in my code.

I wonder if a better solution (while we are waiting for true non-empty collections) would be to have a property/method on Collection which converts empty collections to nil. That is, you either get back a collection which is guaranteed not to be empty or nil.

var nonEmpty : Self? {
    return self.isEmpty ? nil : self
}

Then the call site for guards would look like this:

guard let nonEmpty = myCollection.nonEmpty else {...}

(Ben Cohen) #20

One other thing to note about algorithms not currently in the std lib: this is to some extent a result of waiting for ABI stability. Currently apps must ship with a copy of the std lib, so there is a cost to be weighed in including lesser-used but important complex algorithms, which also tend to be longer. ABI stability will allow for the library to be shipped once for all apps, reducing this particular concern.


(David J. Ehrlich) #21

Collections are not the only types that have “isEmpty”. Thus, I’d like to see something like this:

protocol Emptiable {
    var isEmpty: Bool { get }
}

extension Emptiable {
    var isInhabited: Bool {
        return isEmpty == false
    }

    var inhabitedValue: Self? {
        guard isInhabited
        else {
            return nil
        }

        return self
    }
}

For the rationale behind the naming, see:
Empty Set
Inhabited Set

SetAlgebra & Collection should conform to this protocol.
Perhaps these (and others) should as well?

ClosedRange
Data
Dictionary
Range

(Ben Cohen) #22

What useful generic algorithms, spanning those different types, do you see being enabled by this new protocol?


#23

I feel like almost nobody who wants sugar for

guard !myCollection.isEmpty { … }

is going to be happy with

guard let nonEmpty = myCollection.nonEmpty else {...}

I would also say that support for non-empty collections in the standard library is a tricky issue, because any methods that might remove an item would need to drop back to the possibly-empty form, limiting the use cases to situations where you preserve or increase the count. The benefits also seem fairly minor, mostly removing some Optional wrappers and precondition tests. The cost for these benefits is the need to maintain a whole parallel implementation for most/all generic Collection methods, preserving the distinction.

In similar situations, Swift seems to prefer a more pragmatic approach (e.g. preferring to traffic in Int, even for things like counts that can't be negative), instead enforcing these kind of constraints with preconditions.


(Jon Hull) #24

It's not so much sugar as it is clarity around the ! symbol.

I don't think it should be implemented that way. Instead it should probably be a special set of annotations (or something similar) that get carried around with the type. I don't want to derail the discussion with implementation of non-empty collections (which won't come for a while), but I think it could be done without parallel implementations (or at least any written by humans).

It is true that anything which decreases the count would need to be possibly-empty. It is also true that anything that adds to the count will be non-empty. In my experience, the percentage of code which removes things from collections is much smaller than that which adds to them. All of those little checks and wrap/unwraps do add up.


(David J. Ehrlich) #25

As you might surmise, I don't have any specific generic algorithm an Emptiable protocol would enable. I find it interesting for two reasons:

Clarity: I find "isInhabited" to be much clearer and easier to reason about than "isEmpty == false" & "!isEmpty" (& even "isNonEmpty").

Consistency: Across a large code base utilizing a wide variety of types that implement "isEmpty", consistently having "isInhabited" & "inhabitedValue" available is invaluable.

I find the following concise & clear compared to other spellings:

guard let inhabitedStrings = strings.filter { $0.isInhabited }.inhabitedValue
else {
    return
}

Implementing "isNonEmpty" for clarity in Collection misses the opportunity to clarify all the other types that are emptiable.


(Michel Fortin) #26

I too like isInhabited better than isNotEmpty. I find getting rid of the negation makes things clearer than just moving the negation around.

It doesn't necessarily have to come with a protocol though.


(Tim Vermeulen) #27

I've never heard anyone refer to a non-empty array as an inhabited array, is that something people do? Or is it mostly an attempt to get rid of the negation? I generally agree that negations in boolean property names should be avoided because you may end up with double negatives, but that will never happen in the case of isNotEmpty because you'd just use isEmpty if you want to negate it.


(Pierpaolo Frasa) #28

I weakly favour the addition of isNotEmpty, somewhat more strongly that of isEven / isOdd and rather more strongly isMultiple(of:). But I'll agree that the amount of discussion for such minor changes is not proportional to the actual utility derived, especially compared with bigger topics. It's a classic case of bikeshedding. So either:

  • Strictly additive, non-breaking changes that have popular demand / precedent, get added without a lot of bureacracy; or
  • More unambiguous guidelines that can be applied to pitches are defined, and things like isNotEmpty are ruled out faster.

I can understand the reasoning behind the second option (even when coming from sugary Ruby land). At the same time, nothing is stopping anyone from creating a swift-sugar library, including isNotEmpty, isEven et al., which can be what ActiveSupport was for Ruby: a library that is well-known and can be added in order to enable nicer syntax and sugar. If there was an official package index, that would make the situation even better. In time, this library could become something that is well-maintained and used by a lot of projects.


(David J. Ehrlich) #29

As for whether it’s done with any regularity in software development, I’m not sure. I suggest it for two reasons:

  1. It’s a “term of art” in both classical mathematics & set theory, among others.
  2. It’s not a “negation” and thus enhances clarity.

If “isNotEmpty” (or whatever color the bikeshed is painted) is added merely to Array or even just Collection, then I’m against this pitch. But when viewed more generally, as a protocol to which both Collection and SetAlgebra conform (something we can’t add retroactively), then I’m for it.