Allow optional unwrapping in where clause

Well but a where clause should stay only for boolean expressions in my opinion. The pitched feature may potentially look something like this instead for ... in ... where ... if ... { ... }. I'm not saying I'm in favor of the latter but I'm not sure we should break the simplicity of the where clause, because then we should be able to unwrap optionals in every possible where clause (except in places such as extension ... where ... { ... }.

That said, I'm neutral on it's addition in the language. ;)

I'm not sure it was a good idea to have it be (roughly) a boolean expression in an if statement either, but I can't say at this point that it would be inconsistent or strange for it to also be boolean in some similar contexts.

1 Like

I feel like this should be moved into the binding side of the for loop. Maybe we should just allow for multiple patterns to be allowed on for loops?

for let thing, // first thing in binding list is always the thing from iterator
    let something = thing.optionalProperty,
    case let .someCase(binded) = thing.someEnum 
    in collection where binded == 2 {
 }
2 Likes

There's still nesting in both your and my example, and it would be great if we could avoid it.

It will break tons of code, so I guess this is a no go.

Besides, when we look at a for loop, we generally want to see what we're iterating over first, not what we are filtering out.

This will also introduce an inconsistency with the switch operator, in which we'd be still using the where clause.

Why? The pitched syntax is not source breaking in my eyes but completely additive. I'm not sure about for something in vs. for let something in though.

How will expanding the allowed syntax of for-in break code? It would certainly be a major addition, but I don't think we currently allow anything that would conflict with this.

And this is what you're getting. This is just pattern matching. It's basically what happens now on the lhs of the for loop, but we only allow a single pattern to be bound. By opening the number/kind of bindings allowed we're just allowing the for-in more expressivity.

Oh, I misunderstood you.

But still, even if we do this for for loops, what do we do with the switch, do-catch, etc. operators that support where clause too? What I'm proposing is to allow binding in all kinds of where clauses except type constraints. (I've edited the post to be more clear on this.)

I feel where is the wrong place for this kind of behavior. Generally where is used to express some kind of restrictedness to the thing it's modifying. Opening it up to allow variable binding feels like the wrong path to go.

1 Like

Now on the third thought I'd vote against it because it will just make other language extensions like the following one unnecessarily complicated to read and understand if not completely impossible, just because any potential where clause could also be followed by bindings.

I'd rather prefer to pursue something like Erik posted above.

FWIW you can get something that's pretty close already:

for case let (user, pic?) in users.map({ ($0, $0.profilePic) }) where user.hasBirthdayToday {
  addBirthdayCap(to: pic)
  birthdayCapOwners.append(user)
}

It probably doesn't scale very well with more binding though.

Also, it probably helps to see the pattern matching of for-in in practice.

This is actually the full desugared form of a for-in

for case let index in 0..<10 { }

And lets show probably the most common pattern matching done in Swift: matching optionals

// this is still the identifier pattern! nils are not unwrapped
for case let thing in [1, nil, 3] { } 

// now lets start matching optionals
for case let .some(thing) in [1, nil, 3] {}  // No sugar, not really the optional pattern
for case let thing? in [1, nil, 3] {}  // the optional pattern

Optional isn't actually special with for loops, this is also perfectly fine:

enum Thing {
  case one
  case two
}

for case .two in [Thing.one, Thing.two] {
  print("a two")
}

Once you see that, I think it's pretty obvious that, at least with regards to for-in, putting pattern matching in the where seems a bit suspect when the lhs already does some pattern matching.

4 Likes

I'm not totally sure why the where syntax exists on for-in— it seems like sugar for a limited kind of guard statement just inside the loop, and makes it less obvious what happens.

That said, if we have the ability to filter the elements in a loop, I think it would make sense be able to include anything that can appear in a guard statement:

for user in users {
    guard user.hasBirthdayToday,
        let pic = user.profilePic,
        let url = pic.url,
        isValid(url)
    else { continue }
    
    addBirthdayCap(to: pic, with: url, for: user)
}

vs

for user in users where
    user.hasBirthdayToday,
    let pic = user.profilePic,
    let url = pic.url,
    isValid(url)
{    
    addBirthdayCap(to: pic, with: url, for: user)
}
2 Likes

Yeah, there was a proposal long ago to remove where from for-in loops, but it was rejected (thankfully.) I do have mixed feelings about the name. (Part of me thinks it should have been renamed guard)

I think the problem I have with having optional binding on the where is that given for-in's ability to already do pattern matching on the LHS, you would now have a blur between checking a boolean, and matching a pattern on a construct that can already do pattern matching elsewhere. Why not just remove where and merge its functionality with the LHS? That would bring it inline with guard. And because where is settled at this point we can't do that.

So I think the question is do we expand where to match what guard can do, or allow multiple patterns to be matched on the LHS of the for loop (or do nothing of course.)

And could the expression to the right of the in do some pattern matching too, solving SE-0231 Optional Iteration? And if so, can it be done in a general way that is consistent with other pattern related stuff in the language? I started a thread to discuss whether Swift's patterns could be more uniform here.

+1 for where let clauses.

To keep the discussion grounded in reality, here are two examples off the top of my code base.

Example 1: Preparing for a storyboard segue using switch/case/where

Before:

case let destination as UINavigationController:
    guard let destination = destination.topViewController as? HiddenPuzzlesViewController else {
        break
    }
    destination.dataSource = self
    destination.delegate = self

After:

case let destination as UINavigationController where let destination = destination.topViewController as? HiddenPuzzlesViewController:
    destination.dataSource = self
    destination.delegate = self

Example 2: Updating visible cells in a collection view using for/where

Before:

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    guard let cells = collectionView?.visibleCells else {
        return
    }

    for cell in cells {
        guard let cell = cell as? PuzzleListCell else {
            continue
        }
        cell.setEditing(editing, animated: animated)
    }
}

After:

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    for cell in collectionView?.visibleCells where let cell = cell as? PuzzleListCell {
        cell.setEditing(editing, animated: animated)
    }
}

Note that both examples feature shadowing. I would expect it to work like e.g. guard let works inside of a switch/for statement today. Also note that in the second example, the for loop iterates over an optional collection as proposed in the optional iteration thread.

IMO, in both cases, readability is significantly improved.

1 Like

Why not simply:

for cell as? PuzzleListCell in collectionView?.visibleCells {
    cell.setEditing(editing, animated: animated)
}

?

Maybe it should be without the question mark, since the following does work today:

func f(_ anys: [Any]) {
    for case let e as Int in anys { print(e) }
}
let optInts: [Int?] = [1, 2, nil, 3]
f(optInts as [Any])
// will print
// 1
// 2
// 3

So the last example could perhaps be written like this today:

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    if let cells = collectionView?.visibleCells {
        for case let cell as PuzzleListCell in cells {
            cell.setEditing(editing, animated: animated)
        }
    }
}

I've written some for...guard loops in the past, and it happens quite a lot that my first thought is using the where clause, which can perform the check, but can't give you the type safety afterwards.

But imho it would be better not to extend the capabilities of where, and bring for more in line with if and while instead by allowing a comma-separated list of conditions:

for user in users, user.hasBirthdayToday, let profilePic = user.profilePic {
    addBirthdayCap(to: profilePic)
}

What I like about this approach is that it could be extended:

for let sections = optionalSections, section in sections, row in section {
  print(row.title)
}

This would not only allow convenient iteration over optional sequences, but also walking nested structures.

2 Likes

That's a bit of an expansion to the idea of multiple patterns in a for. It's an interesting idea, but I'm not sure how practical it would be given that a lot of times I've used nested loops, I generally want to break the inner iteration at some point. So unless you allow specifying labels to each iterable (which I guess would be possible) that would limit what you could usefully do with this.

TIL! Thanks.

Refactored as follows:

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    guard let cells = collectionView?.visibleCells else {
        return
    }

    for case let cell as PuzzleListCell in cells {
        cell.setEditing(editing, animated: animated)
    }
}
1 Like
Terms of Service

Privacy Policy

Cookie Policy