`isNotEmpty` on Array

I propose adding a variable isNotEmpty on Array, to compliment isEmpty. It would simply return the negation of isEmpty.

public var isNotEmpty: Bool {
    return !isEmpty
}
4 Likes

Hey @dylanmaryk, thanks for the pitch, it's always good to see ideas on how Swift could be made better.

Unfortunately I believe this is one of those pitches that has come up before, and the general guidance we've gotten from the Core Team is that adding to the standard library should only be done if the thing:

  1. Is hard for a user to write correctly or writing correctly has a significant cognitive burden
  2. Doesn't have an equivalent in the standard library and;
  3. Is being wildly reproduced in the wild

This isn't an exhaustive list, and there's always exceptions, but generally changes that just add a negation of an existing API fail hard on #1 on that list.

@Ben_Cohen Do you think we should have some notes on how standard library changes should be thought out. Possibly posing some questions for potential pitchers?

3 Likes

I'd rather would have isFalse on Bool but that was rejected by the argument that == false is good enough, which in your case also may be applied and transformed to .isEmpty == false.

1 Like

That's fair. I simply think this would be some nice syntactic sugar, similar to toggling a Bool.

1 Like

Yeah, personally I'm torn on these kind of changes. One part of me sees how you could quickly bloat the standard library if you don't have some line that must be crossed for additions. But on the other hand some of these changes do improve readability a tiny bit. guard !.isEmpty is probably in the top 3 of most common guards in my code. And while the cognitive burden of that isn't much, guard .isNotEmpty reads nicely.

I would really like to see stats on how many codebases define some kind of negation of isEmpty. Because if it turns out that a majority of large codebases do define this, then I would be on the side of inclusion, if just for the sake of readability.

5 Likes

This is not a trivial question, because there are important languages in the wild that provide negative operators/functions in order to complete their positive counterpart:

  • Swift, C, etc.: x != y vs !(x == y)
  • Python: x not in list vs. not (x in list)
  • SQL: x IS NOT NULL vs. NOT(x IS NULL)
  • Ruby+ActiveSupport: x.blank? vs. x.present?
  • Perl: I should not have googled
  • ...

In this list, Ruby is famous for providing plenty of synonyms: it is not an example a priori. However, SQL and Python are not know for being fancy.

Could this apply to isEmpty? In the light of the first rule of the Swift API Design Guidelines, "Clarity at the point of use", which one below is the best?

if !array.isEmpty
if array.isEmpty == false
if array.isEmpty.isFalse
if array.isNotEmpty
if let e = array.first
1 Like

we definitely have that in our codebase, and that was before any of us every looked into these forums etc.

2 Likes

I would prefer some kind of NonEmptyCollection type, with non-optional first/last/min/max, etc. You could create an optional property on regular Collections for it, which means you only need to dynamically verify once that the thing isn't empty.

e.g.

struct NonEmpty<C: Collection> {
  var first: C.Element { return base[base.startIndex] }
  // ...etc
}

extension Collection {
  var nonEmpty: NonEmpty<Self>? { return isEmpty? nil : NonEmpty(base: self) }
}

let myArray = [1, 2, 3, 4]
guard let nonEmptyArray = myArray.nonEmpty else { throw SomeError.UnexpectedEmptyArray }
// use .first, .last, etc without Optionals.

8 Likes

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.

7 Likes

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

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.

1 Like

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

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.

9 Likes

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.

16 Likes

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?

1 Like

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.

3 Likes

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.

4 Likes

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 {...}
1 Like

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.

2 Likes

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
1 Like