Another try at allowing optional iteration

You could do (ignoring bikeshedding of the name and my probably questionable workaround for referencing Sequence.Element):

extension Optional where Wrapped: Sequence {
    func flatten<Element>() -> AnySequence<Element> where Element == Wrapped.Element {
        switch self {
        case .none:
            return AnySequence<Element>([])
        case .some(let wrapped):
            return AnySequence(wrapped)
        }
    }
}

for element in sequence.flatten() {
    print(i)
} 
else {
   print("no elements")
}

e.g., there's nothing that prevents an explicit method or function (or even a prefix operator) from doing a conversion.

The draft implementation: [WIP | SE] [Parse][Sema] Optional iteration by AnthonyLatsis ¡ Pull Request #19207 ¡ apple/swift ¡ GitHub.

The current syntax is for?.

let array1: [Int]? = nil

for? element in array1 { ... } // Does nothing

let array2: [Int]? = [1, 2, 3]

for? element in array2 { ... } 
// Equivalent to  
for element in array2! { ... }

4 Likes

I had pitched this previously, but the discussion was bogged down by
purists who claimed that thinking of optional as a sequence of 0 or 1 elements was somehow more useful, and we should aim for that.

Perhaps I'm a bit late. But since ! works in that position:

for element in array! { ... }

Wouldn't it make sense to allow ? at the same place:

for element in array? { ... }

That reads much better to me. But, just like optional chaining, it'd require a special grammar since array! is an expression while array? isn't.

5 Likes

I think interpreting the problem in that particular way from the very start (conditionally conform Optional to Sequence) wasn't the best idea. From what I see, the conversation broke up into discussing other unrelated conditional conformances and whether to conditionally conform Optional to Sequence or not, but no one really expressed any disapproval for having optional iteration.

There was a decision between for? and for in?. Because we are aiming to express optional iteration, not inclusion or something along those lines, I went for the first option.

It would make sense if it were valid grammar in Swift. Having it exclusively for this case is somewhat inconsistent and can be confusing.

There was an implied disapproval for how it should be iterated. Under this nonsensical idea of treating an Optional as a sequence of 0 or 1 elements, this would happen:

let a: [Int]? = [1, 2, 3]
for i in a { // only 1 loop iteration occurs
    print(a) // => [1, 2, 3]
}

I'm not familiar with the grammar, but as an experienced Swift dev, this feels a lot like the optional pattern matching you can use in switch, which allows matching on something? when switching on an optional. Makes sense to reuse the same pattern for looping.

3 Likes

Swift is sometimes intuitive enough to make us not pay attention to such things, thanks for mentioning that.

This goes along the (perhaps not so good) suggestion that Ranges could return optionals instead of crashing when startIndex > endIndex. Having to resort to strides / while loops in algorithms where this can happen doesn't feel right to me and optional iteration is a possible solution.

My take on this is that I would be oposed to for in magically skiping nil sequences.
The alternative of making optional behave as a sequence gets rid off magic in the for in construct, but obviously merges conecptually an optional with a sequence which seems to bot be desired.
This is not a high priority for me but if it moves forward I think it should be with

for a in array?

that minics the syntax we have in pattern matching constructs.

1 Like

I feel like people that mention this magic forget about the actual purpose of optionals. Swift constructs that offer special treatment for optionals are all about either skipping or continuing with the unwrapped value.

As an alternative, the disambiguation for the case of Optional ever becoming a Sequence could be tackled with an optional binding pattern we are all used to, i.e.

let array: [Int]? = [1, 2, 3]

for element in let unwrapped = array { }

It naturally reads as a shortcut for if let array = array { for x in array { ... }}. Although it comes with its own drawbacks, of course.

I'm really opposed to this. Every special treatment that optionals get from the language today, works for every optional. This suggests adding special cases for specific constrained versions of optionals, i.e. optional sequences.

This adds complexity, not simplification. And why not treat optional strings specially? Or ints. A lot of types have an implied "empty" or "identity" element. Why not support those as well? Should optional transforms be treated as the identity transform? Optional integers as zero? What's special with optional sequences, that makes us want to treat them as empty sequences?

On top of that, this already confounds the current mental model of optional as a de-facto functor, and will certainly conflict an explicit functor when the language eventually evolves to fully support that concept in a generic way.

3 Likes

We're not treating optional sequences as empty here, similarly to how optional invocation is not treated as an empty function, for example, array?.foreach when array == nil. The loop is either run if there is a value, and skipped otherwise. It's the same optional binding pattern we have for if statements, expect for the obvious restriction on types we need for iteration. It's important not to overlook that this change affects only a native language statement – for-in –, not some standard library operator or custom type.

1 Like

The optional binding if let-pattern works on every kind of optional. Optional function invocation works on every kind of return value, with no special treatment for specific specialized kinds of optional.

This suggestion is new and not like anything we've seen in the language thus far, and will also overload the mental model with additional complexity. Why will for…in on optional sequences work on the underlying wrapped sequence, while map on the same type works on the optional itself?

And again, why do we stop here? Why not treat empty optional transforms as the identity (aka noop) transform?

Clearly enough we can't iterate on something that doesn't conform to Sequence. We have to take the purpose of the for-in statement into account.

Then again, this proposal is about enhancing a language statement. Your question would be on point if I were, for instance, proposing that nil + "hello" == "hello". If we're going to talk about something along those lines, a separate thread is more appropriate.

1 Like

I think your slippery slope argument is ill-founded. Using our best judgement to improve optional's usability is a good thing.

They would be. They're sequences too.

That's actually not a bad idea. We could generalize this behavior to allow types to specify a canonical empty element, that's equivalent to nil. I haven't thought about it in depth, but this could be fruitful discussions. It's not nearly as absurd as you make it out to be, and it certainly doesn't warrant a complete shutdown of the line of thinking.

Why would we? No one suggested it, and there's no motivating purpose behind doing that. We already have the nil coalescing operator for easily specifying a default value for an Int. That's easy for an Int. That can be hard, or even impossible (how do you initialize an empty db record, of an opaque type returned by a library?), in the case of sequences.

If the element is semantically empty then couldn't one already conform it to ExpressibleByNilLiteral?

No, strings wouldn’t be treated specially. Only the sequence. You’re confounding strings with sequences, but just because they’re also a sequence doesn’t mean strings are given special treatment. An optional string wouldn’t be treated as the empty string, but the optional string as a sequence would be treated as the empty sequence of characters. Those are not the same.

There’s a reason Swift absndoned the legacy of objective c where passing messages to nil pointers would be a no-op, and where false, nil and 0 were treated the same.

Anyways. My argument isn’t about utility, but consistency. You’re suggesting adding complexity by adding language support for a subclass of optionals. One thing is to add conditional conformance to a type, another is to add conditional language features to a type.

If you want to go down that route a better solution would be to add a general way of expressing a trivial “empty” element for types through a protocol, that any type could conform to, and add syntax similar to ?? that would evaluate to the empty value. This would also be useful in reduce among other things. And be consistent.

1 Like

To expand what was written earlier, this is not really about expanding Optionals in any way. Rather, it's about expanding the language support for Optionals in a consistent way.

We have if let and if var for conditional binding of Optionals, where the true branch is skipped when the binding fails (i.e. the Optional is nil).

This pitch is about extending for ... in ... the same way. To whit, the body of the loop should be skipped if binding fails. It's not treating Optional sequences differently than other Optionals. It's extending conditional binding to the for .. in ... construct.

5 Likes