SE-0231 — Optional iteration

0.5. I really don't like the proposed syntax since it doesn't fit with Swift's current style. Adding ? to such a common keyword has the risk of making a simple for loop hard to understand for newcomers. I am also in favor of adding the optional mark to the in keyword which better shows the action that the loop performs. It gives more clarity when reading the syntax:

for i in? sequence { ... }

I would read this "for each i in the optional sequence", do something.

Yes.

Yes, but as I said the proposed syntax doesn't meet the current Swift cosmetic in the way I see it.

N/A

Read the proposal multiple times as well as all the posts in this discussion.

Interesting. The fixit seems coordinated and suggests the following, which does work. I have to admit this is the first time I see an actual use case for an "unterminated optional chain".

switch val {
case .a?: print("rrrrrr")
case .b?: print()
default: print()
}

However, this works as well, which is suspicious:

switch val {
case let x where x == .a: print()
case let x where x == .b: print()
default: print()
}

Could the reason the original statement fails be that the compiler fails to recognize the synthezised Equatable conformance when pattern-matching? @Ben_Cohen could you help us figure this out?

2 Likes

Wow, I never knew that syntax existed. I guess that does kind of lead me to favour the for x in val? syntax.

I suspect it's more that the type checker is for some reason reluctant to upgrade .a to an optional in order for the ~=<T: Equatable>(lhs: T, rhs: T) to kick in (for T of AB?). @rudkx might know why.

It does happen with variables (note it can't be exhaustive in that case):

let val: AB? = .a
let aye = AB.a
let bee = AB.b
switch val {
case aye: print() 
case bee: print()
default: print()
}

I wouldn't describe that syntax as an unterminated optional chain. It's more like .a? is shorthand for .some(.a). Hence this is exhaustive:

let val: AB? = .a
switch val {
case .a?: print("rrrrrr")
case .b?: print()
case nil: print()
}

in the same way this is:

enum AB { case a,b }
enum MyOptional<T> { case some(T), none }

let val: MyOptional<AB> = .some(.a)
switch val {
case .some(.a): print("a")
case .some(.b): print("b")
case .none: print("none")
}
3 Likes

I support it. This is something that would have come in handy from time to time in the past. I also prefer placing the ? after the sequence identifier for the reason that was mentioned earlier: consistency with which syntax produces vs. unwraps optionals.

As an additive change, I think so.

Yes.

I haven't.

Read the proposal and the thread.

What would be the downside if syntax did not change, and the for loop simply handled an optional iteratator as if it was empty. ie, nothing to iterate over???

I am against any “for?” Or “in?”. These are not easy for new comers, and I can’t think of why this can’t just work with existing for-in syntax.

Switch statements handles optionals without any special syntax.

The downside would be that developers might accidentally use this feature, not realizing that the sequence they’re iterating can be optional, and thereby introduce a bug.

This behavior should be explicit for the same reason optional chaining is explicit: to avoid accidentally using the feature without realizing you’re using it.

3 Likes

I see.

Does for? means someone will want a while?? I fee like the requests for the conviences of unwrapping would be endless if this is accepted. Nil coalescing operator is much clearer and more consistant IMO.

5 Likes

Yeah, I realized that, the "unterminated optional chain" was just my way of describing the ? :slightly_smiling_face:

I will definitely update the proposal to emphasize this - as already mentioned, switch is always exhaustive, hence you still have to handle the nil case. Optional iteration implies skipping optionals, which is different.

1 Like

To make a slippery slope argument you really need to show there's a slope to slip down.

There is already good handling in while for optionals via pattern matching:

var iterator = [1,2,3].makeIterator()
while let x = iterator.next() {
   print(x, separator: ",") // prints 1, 2, 3
}

And since optional chaining is flattening, you don't really get the same problem with while that you do with for:

var a: [Int]? = .some([1,2,3])
var iterator = a?.makeIterator()
while let x = iterator?.next() {
  print(x)
}

If while didn't already have these features, would it make for a suitable evolution proposal to add them? Definitely!

For the sake of argument, let's take a different case: adding together two optionals. This is actually pretty hard to figure out and a pain to write even if you know how. There are lots of ways to solve this: overloading +; adding a +?; adding the ability to lift any infix operator to take two optionals; adding a kind of free equivalent of Optional.map that takes two optionals. Rather than be worried that adding optional sugar for for leading to more proposals to tackle things like this, I think this is an area worth exploring since it's a source of pain for developers currently.

7 Likes

What is your evaluation of the proposal?

-1. I don't consider iteration over optional sequence to be so special to get its own sugar. Moreover, i think it will have a negative impact on the language (more below).

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

No and the proposal provides no proof or explanation that this is a significant improvement over the existing, so-called "workarounds".

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

No. In my opinion, this proposal will have negative impact on a couple of aspects:

1. Learning Swift

Right now, unwrapping optionals and iterating sequences are two distinct ideas in Swift and can be easily understood in isolation. Beginners learn how to do these things and then they can glue this knowledge together when facing an optional sequence.

Introduction of special control flow operator for this case will only make the language more complex and will raise further questions, like where's while?, switch?, if?.

2. API design

After this feature is introduced, thus making optional sequences a "blessed" type, I feel that API authors will be more inclined to misuse optional sequences where other types would make more sense.

For instance, I think usage of optional sequences will increase in return types and completion closures, where non-optional sequence wrapped in Result type, or throwing function would be a better choice.

3. Safety

This is my most important point, I'll do my best to explain what I mean. TL;DR: For me this syntax shares similar safety implications as the ability to send messages to nils in Objective-C.

Going through explicit unwrapping or coalescing has the advantage that... a developer must actually write that. And as they write it, they usually think about the difference between an empty and an optional sequence. In the end, they might decide to deal with the optionality in an earlier stage.

With the introduction of for?, the difference between iterating on a non-optional empty array and an optional array becomes blurred. So blurred, it might go unnoticed during development and code review and might have serious complications, where a different code path is required for these two cases.

Now, re. the proposed fix-it — I think fix-its should increase program safety and teach developers how to properly handle cases that caused them. In my opinion, this particular fix-it should draw developer's attention to the difference between optional sequences and non-optional empty sequences. It should not suggest omitting one of these cases silently. There's a reason for that when you write try, Swift doesn't suggest adding try? — instead, it suggests adding a proper do/catch.

To sum up, I feel this proposal will make it harder for developers to catch and distinguish the use of optional sequences, thus making it easier to introduce bugs in our products. Swift, with its opinionated focus on safety, should not go in that direction.

Not all developers are experienced enough that they won't fall into the aforementioned trap. Swift should make it harder for less experienced developers to make mistakes. And this proposal undermines that mission.

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

Nah, I don't think any other language has it.

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

I read the proposal, scanned the initial pitch thread and read discussion in this thread.

8 Likes

FWIW TSPL refers to this as an "optional pattern".

3 Likes

I don't see the parallel. A while statement takes a boolean expression. It is not iterating over a sequence. It is perfectly valid to just use ?? false, if the condition is an Optional. This syntax has none of the downsides already enumerated for ?? [] iteration.

2 Likes
  • In a switch statement the var? means .some(var)
  • In optional chaining this looks very similar. foo?.bar looks a lot like .some(foo).bar

I think the discussion/proposal here should be more about harmonizing this pattern and extending it to more places.

for a in collection? {…} should be synonymous with for a in .some(collection) which seems like it should work naturally.

I know that the collection here is not "matched" like in a switch statement, but this notion of var? being .some(var) seems compelling to me.

5 Likes

With the same logic, we could justify to completely remove for loops, which are just a similar amount of sugar for while (which can deal with optional collections easily...).
So do we keep the regular for just because of backwards compatibility, or because it's considered to be useful?

Those explanations may be missing in the proposal, but you can look them up in this thread. Name the one you prefer, and I'm confident someone can show its flaws ;-)

Right now, there is a very easy solution to deal with optional collections:
Just add a ! and everything is fine (until it isn't fine anymore).
I can't remember a single situation in Swift where you can't use ? as "safe" alternative to ! - and for iteration of missing collections, there is a very obvious way how to handle this safely.

Well, when you have to put the question mark next to the collection you want to iterate, I don't think many people will recognize this as a separate operator at all - it is even possible to declare the desired behavior without changing the compiler at all:

postfix operator -?
postfix func -?<S: Sequence>(_ sequence: S?) -> AnySequence<S.Element> {
  if let s = sequence {
    return AnySequence(s)
  } else {
    return AnySequence([])
  }
}
1 Like

With the same logic, every pattern which appears more or less regularly should be given its shorthand convenience syntax.

Usefulness is not the only metric we have to measure when we evaluate a proposal. The question "Is the problem being addressed significant enough to warrant a change to Swift?" tells us that we have to have a problem.

for, and try?, do solve many problems. Without them, the amount of boilerplate blurs the intent of the code, and we are required to declare temporary variables:

// meh
var iterator = sequence.makeIterator()
while let element = iterator.next() {
   ...
}

// meh
let value: Result?
do {
    value = try func()
} catch {
    value = nil
}

It is much less obvious that the situation that the proposal aims at "solving" is an actual problem.

Should for? (or for in?, or for in sequence?) be added to Swift, I'm sure I'd use it sometimes: convenience is convenient.

And I also see myself teaching my java-kotlin coworkers this new Swift oddity. I don't quite think they'll be impressed.

That's why I was asking above if we are planning something with more ambition and rationale. It'll help me telling a real story.

In what way is it a new oddity? It's just another way to conditionally do something if the Optional has a value. That affordance already exists in other forms.

I'm not a fan of the proposed syntax, but the feature itself isn't new to Swift.

You're right. The compiler is indeed already full of little adaptations for optionals, usually invisible (such as T which feeds functions of T?, etc, etc).

Swift already has many idiosyncratic patterns which don't quite translate in other languages. Our optionals, enums, protocols, etc. make Swift special.

And there are some idiosyncrasies that err on the side of oddities:

switch x {
    case let x?: ...
    ...
}

They are not self-documenting at all. You have to learn them. You have to teach them when you read code with a fellow developer who happens not to be familiar with them. Your fingers sometimes hold on a second above the keyboard before you start typing them.

They don't quite contribute to making Swift special. And it is not obvious at all that they belong to the "bonus" column.

2 Likes

This fits into Swift's idea of progressive disclosure. You can easily do the same thing via the if+for constructs, which you will learn almost immediately. When you learn a bit more, you can use the shortcut to make your code easier to follow (less nesting). It's not such a radical departure that you couldn't puzzle it out if you came across it in someone else's code.

3 Likes