SE-0231 — Optional iteration

Kotlin has orEmpty - Kotlin Programming Language, so they already have sugar for iteration over optional collections.

2 Likes

It's related, but not quite the same. An explicit orEmpty() method belongs to the standard library, not to the core language. It is at the same level as ?? [].

EDIT: it's a regular method call. It is much less involved and impactful than this proposal.

When this is already in a codebase someone is being introduced to, they don’t have the luxury of building up to that syntax. I think the question of “is this worth the complexity” is important.

I don't think that's a fair argument. There's a lot of syntax (hello if case let...) that's going to throw a complete newbie for a loop, but we aren't recreating BASIC. As long as a newcomer doesn't have to know about the peculiar-looking syntax to get the same effect, he can learn it when he has to.

3 Likes

Yes, but i would argue those features have passed the “is it worth it” bar, and that’s why their odd syntax is accepted.

I’m also making the argument that progressive disclosure doesn’t at all cover the case of people that need to read other people’s code.

To me this feature seems like another layer of complexity for little benefit.

  • What is your evaluation of the proposal?

+0.5 given the alternative for element in sequence?

  • Is the problem being addressed significant enough to warrant a change to Swift?

I'm not sure; it's not one that I've encountered often, and I'm sympathetic to the argument about optional sequences as a design smell. But I want to give other people the benefit of the doubt about how much they would use it. And see next question.

  • Does this proposal fit well with the feel and direction of Swift?

Yes, given the ? being applied to the value, not the keyword, I think it fits snugly with other Optional handling. Putting the question mark on the value looks like a pattern match to me, like the case .a? syntax that was mentioned.* I think it's an easy thing to understand and makes the statement more succinct without loss of clarity, like other Optional sugar.

I think this is a nice affordance for those who want it, without causing undue harm to those who would prefer not to use it.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

n/a

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I read the proposal and the review thread through.


*Although saying this makes me think that the ideal solution might be if this really were sugar for a pattern match. for element in seq where case let .some(seq) = sequence perhaps? But I don't want to get off-track.

1 Like

Strings are sequences, and treating a nil reference and an empty string as identical values is something that annoys Oracle devs. Let's not do that in Swift.

3 Likes

@Tino Good idea!

But using an initializer might be easier than bike-shedding the .orEmpty() method or -? operator.

extension AnySequence {
  /// Creates a new sequence that wraps and forwards operations to `base`.
  ///
  /// - Note: An empty sequence is substituted for a `nil` argument.
  @inlinable
  public init<S: Sequence>(_ base: S?) where S.Element == Element {
    if let base = base {
      self.init(base)
    } else {
      self.init(EmptyCollection())
    }
  }
}
let range: Range? = 0 ..< 10
for _ in AnySequence(range) {}

This pitch is about an obvious (but concise) solution for a simple problem, and although I think it would be a good idea to add that init to AnySequence, it is inferior as a way to ease iteration of Sequence?:
It is verbose, yet lacks clarity, and it allocates an object that isn't actually needed.
The approach would work with non-optional collections as well, but I don't think we actually want that (imho no one actually wants the Any...-types at all - they are just a necessity).

The snippet is just meant to be be an illustration that the core of this proposal isn't a groundbreaking change of the fabric of Swift, but can be very easily introduced by anyone with basic knowledge of the language:
It is already possible to iterate over Sequence? just by adding a single character (compared to iteration of Sequence), so anyone baffled by this will have a hard time with Swift.

So, you can have implement the desired behavior as a library feature, but that has big disadvantages:
It isn't verbose, but it also allocates a useless object - and it forces you to choose an arbitrary operator instead of the established ? to deal with the Optional.
Additionally, the compiler won't warn you when using the Optional-sugar on a Sequence that isn't optional at all.

Adding some lines of code to the source of swiftc doesn't automatically increase the complexity for its users - it can actually make things simpler, because there is a well established meaning of ? and ! with regard to Optionals.

1 Like

@Tino The proposal's implementation also offers a fix-it (i.e. use for? instead of for).

I'm not sure how anyone would discover the AnySequence initializer.

I would also like to draw attention to the fact that an stdlib-side change is, above all in my opinion, ABI dependent. A rather dicey path that is for something proposed as an explicitly spelled fundamental change: as the clock ticks there would be less time to decide on a relatively worthy solution, and once the time comes, there's next to no space for maneuver if we felt like it.

If the preferred solution was to add something to the standard library, I'd recommend doing it by adding a specific simple low-cost type that just wraps the sequence, rather than re-using AnySequence. Type erasure can be a big performance optimization barrier and there's no reason to pay that cost (or ask the optimizer to work overtime to eliminate the cost).

4 Likes

-1 for the proposal.

I second the argument made by some people that this is weird, non-obvious syntax that has to be learned and will not be obvious to people who look at other people's code (in addition it will also be hard to google). I prefer language features that are composable, so if you understand A and B, but you never thought to combine A and B in a creative way, you can still understand it and learn from it when you see it for the first time. for? (or whatever variant ends up being adopted) is not composable, it's just special syntax for a rare edge case.

Adding cutesey short-hand syntax for non-composable features is warranted if it's used for very common patterns that people should be encouraged to use. But this IMHO doesn't fit. I think optional collections are almost always wrong (the obvious exception being when you have to deal with Codable). Now I don't deny that there are cases where there's a distinction that you can draw between empty collection and no collection. What I'm arguing is that this will be completely opaque to the user of an API, because fundamentally nil vs. [] just doesn't carry any intrinsic meaning helping specifying what means what. I argue that:

a) in most cases you can just replace nil with [] (iterating or mapping over an empty collection is a no-op, so fits the bill). In many cases, I think people are just not accustomed of understanding that this actually works and makes the code cleaner, and they just make things nullable because it's in their default mode of thinking. This would be a great opportunity for them to learn (e.g. by a linting rule, which swiftlint enables by default IIRC) how arrays (zero or arbitarily many objects) and optionals (zero or one object) are actually kind of similar in an abstract sense, and

b) in the rare cases that you do need to distinguish the two, you'd be better off specifying what happens more explicitly:

enum UserPreferences {
    case notProvided
    case values([UserPreference])
}

or something to that effect.

IMHO the arguments in this thread focus too much on "ease of writing". It's "I have this optional collection and I want to avoid writing boilerplate". But from my point of view, languages should favour features that make code easy to maintain, because writing it in the first place is often the easy part.


I have read the proposal and most replies in this thread.

1 Like

A fixit could also recommend wrapping the type in an AnySequence, I think it's instructive to think about how that would look.

for x in a { ... } // FIXIT: a is optional. Did you mean a?

It seems likely, even for someone who doesn't know this feature, but does know about other similar optional handing, to think "oh, huh, yeah ok, like optional chaining I guess"

Whereas

for x in a { ... } // FIXIT: a is optional. Did you mean AnySequence(a)

is way more likely to be completely inexplicable to someone who doesn't know about AnySequence, and totally non-obvious as to why it fixes it to someone who does.

This also helps see the flaw in the ?? [] approach. If a is not just an array, but some non-constructable type, then the fixit will be wrong. Alternatively, the fixit could only fire sometimes but not others.

I also think it's important to add that it would be a really good thing for APIs if people started writing their own simple collection types, for example instead of following the "enumerate block" pattern. It would be great if String had a .lines computed property, for example. Few of these "API collections" would be expressible by array literal. If these types proliferate, the balance of times that ?? [] works starts to diminish, whereas the ? approach works in all cases. The last thing we need is resistance to creating great APIs that offer collection interfaces just because they don't compose well with things like optional chaining.

10 Likes

@Ben_Cohen If the low-cost wrapper was called OptionalSequence, would the fix-it be more obvious?

Yes. Possibly it would be better to also add an extension on Optional where Wrapped: Sequence to create it.

What is your evaluation of the proposal?
-1. This adds another special case to Swift that I perceive to have very little benefit.

This feels like it's making the use case easy, but not simple (cf. Rich Hickey's excellent Simple Made Easy talk). IMO Swift already suffers from having too many special cases. I don't think there's enough benefit here to justify additional complexity.

Is the problem being addressed significant enough to warrant a change to Swift?
I don't believe it is.

  1. This change seems mostly cosmetic over ?? []. It doesn't aid in correctness. It overs no material benefits. The proposal mentions "considerable drawbacks", but doesn't justify that claim.

  2. IME most of the time, people shouldn't be using optional sequences: they should use an empty sequence instead of nil. If you do need to distinguish between nil and empty, then you probably wouldn't want to use for? anyway—you'd want to keep the if so you could have an else case.

Does this proposal fit well with the feel and direction of Swift?
It doesn't fit with what I hope is the feel and direction of Swift.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
No

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I read through the proposal, skimmed this thread, and thought about it for a few days.

4 Likes

I am only lukewarm in support of this proposal and won't feel bad if it isn't accepted. However, I am seeing the same opposing arguments repeated in the thread without the previously stated rebuttals being addressed at all.

This does not address sequences for which a default cannot be constructed. The hypothetical String.lines API is a great example.

It also does not address the benefit of moving the conditional nature of the iteration to the front of the statement. These are both material benefits. We can discuss whether they are significant enough to warrant acceptance of the proposal or not, but we should not deny their existence.

Nobody is arguing that optional sequences should be commonly used. There are a number of ways to end up with an expression that produces an optional sequence in Swift. Optional chaining in particular has been mentioned as a common source of optional sequences.

The proposed feature is orthogonal to the best practice of avoiding actually storing an optional sequence. A valid argument against the proposal should address expressions that produce an optional sequence more broadly. Citing a best practice of not storing an optional collection is insufficient as an argument against the proposal.

8 Likes

I don't think that's so. The proposal is for a convenience and so it is absolutely reasonable to opine on it on the basis of how often it will actually be more convenient, and also on the basis of whether we are making the wrong things more convenient, and thus encouraging poor code.

If storing optional sequences is rare and/or bad, then that's a lot of use cases where this doesn't apply or is a mistake.

For optional chaining, if string?.lines is your example of an optional sequence where this proposal would be useful, well, I'm unconvinced. Once again, existing code: (string ?? "").lines is much more straightforward about spelling out what the programmer actually wants, and we're talking about only 6 or so additional characters.

This proposal shortens things that don't need to be shortened and adds a special case for something that (in my opinion) generally only comes up when you're doing it wrong.

(As discussion has gone on and I've become more convinced of the lack of good examples here, I'd like to change my -0.5 to a strong -1.)

1 Like

That's a fair point. But that's a very small use case, so I think my criticism is still valid.

I don't see how that's beneficial.

I think it's a valid criticism when considering "[if] the problem being addressed [is] significant enough to warrant a change to Swift". I'm arguing that I don't think it's significant enough because IME this pain is usually the result of poor API design or legacy APIs

1 Like