Allowing pattern bindings in `for` loop `where` clauses

I was just writing the following code in a UIViewController:

for child in self.children {
    if let delegate = child as? SomeDelegate {
        // ...
    }
}

Given that our linter is set up to warn about the "single if inside a for loop" construct, I automatically refactored this to

for child in self.children where let delegate = child as? SomeDelegate {
    // ...
}

Which resulted in an error at the let:

error: Expected '{' to start the body of for-each loop

Is there any technical reason that this isn't supported? I couldn't find anything under "where let" on the forums; if there's been a previous discussion I missed it!

1 Like

You should be able to write this as:

for case let child as SomeDelegate in self.children {
  // ...
}
18 Likes

Nifty! I'm not sure I like the backwards reasoning required to mentally parse that (over the for-if option available today), but good to know that it's possible. Thanks @ole!

3 Likes

I had a slightly different pitch some time ago where I wanted a single where clause to combine multiple cases. Now the example from @ole makes me thinking if it would also make some sense in a for loop, although they already can have where causes at the end.

Here is the idea I mentioned before:

Could be nonsense though. :thinking:

1 Like

I still have a hard time wrapping my head around the case let construct. Maybe it's just because as words they don't make much sense together, but it's just not obvious to me how it works and every time I look it up I forget it soon after...

14 Likes

That does not seem obvious at all? It sort of hurts to read it too :)

1 Like

Yeah, if case and for case constructs always end up deeply unsatisfying to me. They make some sort of sense if you just imagine them as a case label inside a switch over whatever sits on the right-hand side of the =, but it always requires me to look up the syntax as well.

4 Likes

I don't find the case syntax intuitive, either. I'm not sure the following is 100% correct, but I believe this is the logic behind the syntax:

  1. Patterns, i.e. the conditions you write in a case clause of a switch statement, are a universal feature of Swift that's used in tons of places: switch, if and guard, for and while loops, catch clauses. Even in a plain assignment such as let x = 1, the token x is an identifier pattern.

  2. Patterns can be grouped in two categories: irrefutable patterns (those that always match) and refutable patterns (patterns that may not always match).

    Here are some examples of for loops using irrefutable patterns:

    for x in array (x is the pattern)
    for _ in array (_ is the pattern)
    for (a, b) in arrayOfTuples
    for var x in array

  3. In many places where patterns are used, we write case [pattern] to introduce the pattern. case clauses in switch statements are the obvious example.

  4. for and while loops only require the case keyword for refutable patterns. We can omit case if the pattern is irrefutable, i.e. it matches unconditionally.

15 Likes

That's almost certainly what prompted this dedicated website: http://goshdarnifcaseletsyntax.com

8 Likes

This only seems to work for this example? As in I can't see how to rewrite this code in the same style?

let children: [Any] = ["i", "hi", 1]


func transform<T>(_ it: T) -> T? {
	return it
}

for child in children {
    if let child = transform(child) {
		print(child)
	}
}
1 Like

You can write

for case let .some(child) in children.map(transform) {
    print(child)
}
// or
for child in children.compactMap(transform) {
    print(child)
}
1 Like

This isn’t quite the same situation as the original example because you're creating two bindings: first, the outer child is bound to an element in children, and then the inner child is bound to the return value of transform(child).

for loops don't support multiple consecutive patterns (e.g. separated by commas, as in if or guard statements), so it's not possible to express this directly in a for loop. @MasasaM has shown two viable alternatives that solve this by changing the sequence that's passed into the loop.

To preserve the semantics, it would also have to be

for child in children.lazy.compactMap(transform) {
    print(child)
}

If transform were expensive and children large, we might not want to compute the map for the full array up front. (Or, the body may even have an early exit to avoid computing the entire map at all!)

I'll note that we are in #evolution:discuss, so we're not limited by what's currently possible :slight_smile:. Allowing pattern bindings in the where clause would IMO read much more cleanly than the lazy.compactMap version, or any of the case let alternatives proposed.

A downside I can see is that it would then suggest that where clauses in switch statements should also support pattern bindings, which seems redundant since you can also bind patterns in the case expression immediately preceding the where. So we'd either have to separate these two forms of where, or allow pattern binding to happen in two places in the case statements.

1 Like

I'd suggest to not put them together.

case in Swift means "try to match the following pattern against a value": the pattern (that is, what's on the right of case) part is consistent in all case statements, while the value part depends on the context.

When switching, the value is the one you're switching on:

switch value {
  case patternTheWillBeMatchedAgainstValue:

When iffing, guarding or whileing, the value is the one on the right of the = operator that follows the pattern:

if case patternTheWillBeMatchedAgainstValue = value {

I think, as many do, that this could be better. It's rather strange to see a = there, because that's not an assignment. The other problem, here, is that the value is spelled after the case statement, which means that, to get proper autocompletion and everything, you should write first

if case  = value {

then put the cursor after case and enjoy autocompletion.

Maybe a better way would be something like:

if value match case patternTheWillBeMatchedAgainstValue {

Anyway, when foring, the value is the Element of the Sequence that's being cycled:

for case patternTheWillBeMatchedAgainstValue in sequenceOfValues {

because for doesn't allow for multiple patterns to matched, it's more limited than, say, while, but it's generally good enough.

But the fact that for allows for a pattern to be matched against the values of the sequence allows for code like this:

for case 3 in [1, 2, 3, 1, 2, 3] {
  print("a 3 was found")
}
/// prints "a 3 was found" twice

Notice that, thanks to how case works, you can add assignments in the middle of a sequence of patterns, because let x = is also a pattern (sorry for the convoluted code):

enum MyError: Error {
  case this
  case that
}

let stringResults: [Result<String, MyError>] = [
  .success("11"),
  .success("a2"),
  .failure(.this),
  .success("2"),
  .failure(.that),
  .success("bb")
]

if let firstResult = stringResults.first,
  case .success(let value) = firstResult,
  case let count = value.count, /// interesting line
  let intValue = Int("\(count)") {
  print("A value was found with count \(intValue)")
}

If you just wrote let count = value.count it wouldn't compile, because we need to tell Swift that's we're matching against a pattern there (thus, the need for the case keyword).

4 Likes

Just do this

for child in self.children where child is SomedDelegate {  
    //  ... 
} 
1 Like

If you want to utilize the requirements of SomeDelegate within the body of the for loop, you'll either have to conditionally bind or force cast inside the loop anyway:

for child in self.children where child is SomedDelegate {  
    child.someDelegateMethod() // error!
} 
3 Likes

Thanks, @ExFalsoQuodlibet! I think I'm finally getting it.

It's my duty, sir

1 Like

I came across a few interesting related threads:

In 2018, @broadway_lamb pitched "Allow optional unwrapping in where clause":

In 2016, @Joe_Groff provided some context why the if case and for case syntax exists:

And another 2018 discussion whether the various pattern matching syntaxes can be unified:

1 Like