Another try at allowing optional iteration

I think perhaps the confusion, and strong opposition, here is due to the earlier half of the thread proposing that the optional sequence is implicitly skipped if nil, which I wasn't in favour of at the time. The latter half of the thread seems to have settled on having some sort of syntactic marker, which should make it acceptable to more people. I'm personally not sure what the particular syntactic marker should be, though, out of the 3 or so that have been floated.

Personally, I like for x in xs? since it feels like the existing optional pattern matching syntax.

2 Likes

I favor for let x = xOpt ..., because it matches the binding within an if statement. I think newcomers would find it more logical as well.

I have to admit, though, that most times you don't need any access to the bound variable within a loop, which stands in contradistinction to an if statement.

2 Likes

I‘m not sure if I understand this right - can you extend the example?
I would expect that x is a Sequence, but what would be the name of the element inside the loop?

I haven't thought it through. I was merely echoing what Svein Halvor Halvorsen wrote above. If the pitch moves forward, I am sure there will be plenty of ideas. To bring it back to the conceptual stage, I would say that we want to combine the following into one construct:

if let seq = optionalSeq {
    for s in seq {
    }
}

Perhaps for s in let seq = optionalSequence { ... }.

1 Like

No, it is a bad idea. nil means "no value" not the empty or identity value.

2 Likes

Again, doesn't ExpressibleByNilLiteral already do exactly this?

This is what the documentation says

So Apple's official position is "don't do that".

2 Likes

nil has a specific meaning in Swift—the absence of a value. [...] ExpressibleByNilLiteral conformance for types that use nil for other purposes is discouraged.

We're discussing using it for marking the canonical absence of a value for a type that naturally represents that absence. The purely declarative statement that only Optional conforms to it doesn't change that.

1 Like

I was responding to your question " doesn't ExpressibleByNilLiteral already do exactly this?" The answer is no, it doesn't already do this and I cited Apple's documentation to show that it doesn't already do this, at least not in the Standard Library, and they'd rather you didn't adopt it for your own types.

This proposal has been scheduled for review starting this wednesday as SE-0231.

I think that more natural would be for element in? elements similar to value as? SomeType

4 Likes

I have been listening to the back catalogue of the terrific Swift Over Coffee podcast over the past few days where on one episode they mentioned this SE proposal. They did mention for-in? and seemed in most favour of for? but neither of those sat right with me. It took me a few hours before I realised for-in-let-else was, IMO, the correct way of iterating over optionals.

I was delighted to see @Avi had already proposed this option but I’d like to take it further.

Instead of the existing modus operandi:

let maybeArray: [Int]? = [1, 2, 3, 4, 5]
if let defeinitelyArray = maybeArray {
    for element in definitelyArray {
        // maybeArray continues to be [Int]?
        // definitelyArray is [Int] here
        // element is Int here
    }
}

As @Avi suggests, using for-in-let would look like the following:

let maybeArray: [Int]? = [1, 2, 3, 4, 5]
for element in let definitelyArray = maybeArray {
    // maybeArray still accessible as [Int]?
    // definitelyArray is [Int] here
    // element is Int here
}

Taking it further one step, I propose a for-in-let-else construct, which of course, looks like this:

let maybeArray: [Int]? = [1, 2, 3, 4, 5]
for element in let definitelyArray = maybeArray {
    // same as the prior example
} else {
    // unwrapping failed because maybeArray was nil
}

This would also work with underscore assignment, as follows:

let maybeArray: [Int]? = [1, 2, 3, 4, 5]
for element in _ = maybeArray {
    // maybeArray still accessible as [Int]?
    // Careful, unwrapped version of the array is not available here.
    // element is Int here
} else {
    // unwrapping failed because maybeArray was nil
}

Working with optional arrays of optional elements:

let maybeArray: [Int?]? = [1, 2, nil, nil, 5]
for element in let definitelyArray = maybeArray {
    // maybeArray still accessible as [Int]?
    // definitelyArray is [Int] here
    // Careful, element is Int? this time
} else {
    // unwrapping failed because maybeArray was nil
}

Dealing with multiple let statements:

let maybeMaybeArray: [Int]?? = [1, 2, 3, 4, 5]
for element in let definitelyMaybeArray = maybeMaybeArray, let definitelyArray = definitelyMaybeArray {
    // maybeMaybeArray is [Int]?? here
    // definitelyMaybeArray is [Int]? here
    // definitelyArray is [Int] here
    // element is Int here
} else {
    // unwrapping failed because maybeMaybeArray or maybeArray was nil
}

In this case, the for loop iterates over the last Sequence assigned.

As perfect a solution as I wish this were, I can think of two cases of ambiguity that arise with this solution. The first arises while dealing with where clauses, as follows:

let maybeArray: [Int]? = [1, 2, 3, 4, 5]
for element in let definitelyArray = maybeArray where element.isMultiple(of: 2) {
    // maybeArray still accessible as [Int]?
    // definitelyArray is [Int] here
    // element is an even Int here
} else {
    // Are we here because unwrapping failed because maybeArray was nil?
    // Or because !element.isMultiple(of: 2)?
}

I think the else clause should be entered when the unwrapping failed, not when the where clause wasn't met, but YMMV.

The second, if optional elements of an optional array are given the chance to be unwrapped before entering the loop, as follows:

let maybeArray: [Int?]? = [1, 2, nil, nil, 5]
for maybeElement, let definitelyElement = maybeElement in let definitelyArray = maybeArray {
    // maybeMaybeArray still accessible as [Int]?
    // definitelyArray is [Int] here
    // maybeElement is Int? this time
    // definitelyElement is nicely Int though
} else {
    // Are we here because unwrapping failed because maybeArray was nil?
    // Or because unwrapping failed because maybeElement was nil
}

Of course the two ambiguities above can be coalesced into a monster of a for-let-in-let-where-else statement

let maybeMaybeArray: [Int?]?? = [1, 2, nil, nil, 5]
for maybeElement, let definitelyElement = maybeElement in let definitelyMaybeArray = maybeMaybeArray, let definitelyArray = definitelyMaybeArray where definitelyElement.isMultiple(of: 2) && element < array.count / 2  {
    // maybeArray still accessible as [Int]?
    // definitelyArray is [Int] here
    // maybeElement is an Int?
    // definitelyElement is the unwrapped value of maybeElemet. It also satisfies the where clause.
} else {
    // Are we here because unwrapping failed because maybeArray was nil?
    // Or because unwrapping failed because maybeElement was nil
    // Or because !definitelyElement.isMultiple(of: 2)?
}

which I would rewrite as:

for 
    maybeElement, 
    let definitelyElement = maybeElement 
in 
    let definitelyMaybeArray = maybeMaybeArray, 
    let definitelyArray = definitelyMaybeArray 
where 
    definitelyElement.isMultiple(of: 2) &&
    element < array.count / 2 
{
    ⋮
} else {
    ⋮
} 

In conclusion, I understand this proposal has been rejected months ago, however, this was a fun thought experiment to mull. I do hope that my suggestion rekindles this discussion but I realise it might just be wishful thinking. In any case I'd love to hear critique of my proposed solution.

4 Likes