Another try at allowing optional iteration

Allowing optional sequences in a for loop simplifies the flow and syntactic load required when an else statement isn't necessary for the nil case. In particular, it allows to omit one level of nesting.

let foo: [Int]? = nil

for element in foo {...} // iterate or skip if foo == nil
for? element in foo {...} // With a syntactic emphasis

// One of the regular ways
if let foo = foo {
  for element in foo {...}
}

This topic has been discussed several times more than a year ago with no definite results and conclusions as to what I have found:

In short, the arguments against so far are the possibility to use ?? [] , sequence?.forEach and

Most of the participants of the overall discussion proposed syntactic counterparts to emphasize an optional iteration, among which are for? and for in? as the second thread's title hints. However, syntax additions don't really help in coming to a compromise with Chris' quote. Furthermore, I see no need to enforce new, especially optional (non-compulsory) syntax. In contrast, another alternative solution that was briefly mentioned – conditionally conforming Optional to Sequence – doesn't require any language changes.

In case I haven't made myself clear, my approach is an implicit solution: an iteration over an optional sequence should be spelled equally to a regular for loop (for element in optionalSequence) and supported either by the standard library via a conditional conformance or by the compiler itself. Assuming the minor changes required to implement this missing link in Swift's syntactic sugar, I believe a trade-off is more than possible.

2 Likes

@jrose, actually, I have to admit a conditional conformance looks pretty good.

extension Optional: Sequence where Wrapped: Sequence {
    
    public struct Iterator: IteratorProtocol {
        private var it: Wrapped.Iterator?
        
        fileprivate init(_ it: Wrapped.Iterator?) {
            self.it = it
        }
        
        public mutating func next() -> Optional.Element? {
            return it?.next()
        }
    }

    public typealias Element = Wrapped.Element

    public func makeIterator() -> Iterator {
        return Iterator.init(self?.makeIterator())
    }
}
1 Like

If this is desirable, then why not make nil optional integers be roughly equivalent to 0, and nil optional strings be roughly equivalent to "" (e.g. conditionally conforming to StringProtocol), etc? I guess I'm not sure what the underlying principle is for deciding that optional sequences need special treatment here.

5 Likes

That's one of the downsides of a conditional conformance and the reason I was originally leaning towards a compiler-side change which can isolate this interpretation specifically for for loops. Optionals aren't what a Swift Sequence is supposed to be, after all.

Why specifically for-in loops then, though? What is it about them deserves special treatment over any of the other Sequence API, which you can trivially write yourself using for-in loops? This is the part that has always confused me, including during the Set/Sequence discussions where some people were adamant that you should be able to use them in a for-in loop but not call other Sequence methods.

I'm not sure I understand, so I better answer the previous question first:

Since String is a Sequence as well, the conditional conformance would indeed make "" 'roughly equivalent' to nil. Regarding integers and other types that could come to mind – the pitch is about allowing optional iteration for ergonomics reasons.

Sure, I understand that the benefit is a small amount of brevity for the specific case of an optional sequence in a for-in loop that you happen to want to treat the same as an empty sequence. Maybe this is a common use case, I'm not sure. I'm just wondering what the underlying principle would be to decide that supporting this is valuable, versus e.g. being able to call map/filter on the same optional sequence, or add an optional integer to an integer, or call hasPrefix on an optional string, etc. What is special about optional sequences and for-in loops?

Not to confuse people, it is more appropriate to say that a nil optional sequence is treated exactly as nil. There is no magic going on, at least in the approach with a conditional conformance.

I brought up this pitch because I believe the underlying issue has a common use case and the importance of solving it roughly equals the effort required to do so. That is the principle. I was not considering other only slightly related possibilities; they would belong to different threads anyway. I hope this brings some clarity to what is special (nothing).

Are you referring to optional chaining?

No, not optional chaining, I meant you could theoretically conditionally conform optional strings to StringProtocol, treating nil as "", which would also save some characters in some cases. I was just wondering if there was some general principle upon which these decisions could be made, beyond just brevity.

First, I'm starting with the assumption that the creator of whatever API resulted in an optional sequence to be returned had a reason to do so, e.g. a result with no values is different than a nil result. In this case, the language change described would be fighting against their effort to make this distinction, by treating the optional as an IUO within the context of for-in loops (and indeed, any generic function which takes a sequence)

In the case where the consumer of said API has decided that their treating of an empty sequence and a nil optional sequence should be the same, they can state so explicitly.

7 Likes

To play devil's advocate: What's different about for loops is that, unlike (say) map, you can't use optional chaining to implicitly skip a for loop if the collection is nil. The most convenient affordance Swift offers is the ?? operator, which can be clunky if there's no concise way to initialize an empty instance of the same type.

1 Like

Most users of Swift work in an ecosystem where there has been very little difference between nil and things like an empty NSArray, and imho Cocoa uses nil-properties in quite a lot of cases where an empty array would actually be a better fit (but it just didn't matter for Objective-C).
Even if there is a meaningful difference, that doesn't have to be relevant for every use case, so although I'm skeptical towards "silent conversion", a more convenient way to handle the situation would be very welcome:
In real-world iOS coding, I run into the issue on a regular basis; on the other hand, I remember several small proposals which have been accepted as "useful small additions" which I actually never saw used...

1 Like

Sure, but you similarly get no affordance for passing the optional sequence as an argument somewhere. And optional chaining preserves the optionality of the sequence and therefore distinction between an empty sequence and nil. If someone wants to propose a more general mechanism (e.g. a top level function, type initialiser, or method on optional sequences) for turning an optional sequence into a non-optional one in this way then I would probably support it, as it would be more widely useful and seems more principled to me.

The fashionable NonEmpty sequences/collections would not accept that very easily :wink:

Well that seems to be problematic. "" is certainly not the same thing as nil.

And what are the ergonomic shortcomings of for element in elements ?? [] {...}? This is still not at all apparent to me.

Unless we want every casual but faithful user of Swift to be an expert on the entrails of the compiler, it is sensible to assume that this solution is inferior compared to the slightly more verbose

if let elements = elements {
  for element in element {
    // some code
  }
}

My own experience tells me that most developers don't care much about performance anyways, but when it's easy to establish a solution that is optimal in speed, memory usage and tangibility, I'd strongly prefer that.

The change does not fight against or hinder one's will to treat an empty and a nil sequence differently, it provides a compact and intuitive way to express an optional iteration when needed. That is, to treat an iteration (namely a for-in loop) over a empty sequence equally to an iteration over a nil sequence. Still, nothing is fighting against treating them differently.

The trivial shortcoming is the need to append ?? []. Would you type that if you had the opportunity not to? Apart from that, [] is not a general solution. You can treat that as a shortcoming as well (look through the linked threads, it has been already discussed).

Again, we are roughly aiming to treat an iteration (a for-in loop) over a nil sequence equally to an iteration over an empty sequence.

Disclaimer: I'm not in favor of iterating optional sequence.

The shortcoming is that this works only with sequences that can be constructed from an Array literal.

And from a non-empty array literal, on top of that (hence the joke about non-empty sequences).

Was your reply addressed to the root post? If so, what problem do you see in iterating over an optional nonempty sequence?

I see a potential problem in iterating an optional sequence with the same syntax as a regular sequence, because assumptions on the sequence itself may not hold for that same sequence, made nil.

Real-life example with a (guaranteed) non-empty sequence:

// Using an imaginary NonEmptyArray type
let nonEmptyArray: NonEmptyArray<Int> = [1, 2, 3]
nonEmptyArray.first // 1 (not an optional)

// Iterating a non-empty sequence:
var value: Value!
for elem in nonEmptyArray {
    value = ...
}
// legit
value!

Now what happens if we can iterate an optional one with the same syntax?

// Now the nonEmptyArray is nil
let nonEmptyArray: NonEmptyArray<Int>? = nil

// Exact same code...
var value: Value!
for elem in nonEmptyArray {
    value = ...
}
// ...but fatalError
value!

The ?? [] doesn't work either with non-empty sequences (even if they are ExpressibleByArrayLiteral):

let nonEmptyArray: NonEmptyArray<Int>? = nil

for elem in nonEmptyArray ?? [] { // runtime crash
    ...
}

This is why I feel like sugar for optional sequences can indeed fit some situations. But when it breaks, it's spectacular.