Skip to content

Instantly share code, notes, and snippets.

@erica
Last active June 10, 2016 19:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/dd5935eca0d5d06b046184f41d56dd04 to your computer and use it in GitHub Desktop.
Save erica/dd5935eca0d5d06b046184f41d56dd04 to your computer and use it in GitHub Desktop.

Removing Where Clauses from For-In Loops

  • Proposal: TBD
  • Author: Erica Sadun
  • Status: TBD
  • Review manager: TBD

Introduction

This proposal removes where clauses from for-in loops, where they are better expressed (and read) as guard conditions.

Swift Evolution Discussion: [Pitch] Retiring where from for-in loops

Motivation

Upon accepting SE-0099, the core team removed where clauses from condition clauses. The team wrote, "[T]he 'where' keyword can be retired from its purpose as a [B]oolean condition introducer."

Frequency of Use

Where clauses are rarely used. In the Swift standard library, they occur three times, compared to about 600 uses of for-in.

private/StdlibUnittest/StdlibUnittest.swift.gyb:    for j in instances.indices where i != j {
public/core/Algorithm.swift:  for value in rest where value < minValue {
public/core/Algorithm.swift:  for value in rest where value >= maxValue {

I pulled down a random sample of popular Swift repositories from github and found one use of for-in-where among my sample vs over 650 for-in uses.

Carthage/Source/CarthageKit/Algorithms.swift: for (node, var incomingEdges) in workingGraph where incomingEdges.contains(lastSource) {

Confusion of Use

Consider the following two code snippets:

print("for in")
var theArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for x in theArray where x % 2 == 1 { print (x) }

print("while")
var anArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
while let x = anArray.popLast() where x % 2 == 1 { print(x) }

In the first, the where clause acts as a filter, using syntactic sugar for continue when its condition is not met. In while loops, it’s a conjoined Boolean, and will break when its condition is not met. In my experience offering peer support for new Swift developers, the where clause is a source of confusion when it is considered and/or used.

Simplicity of Guard Conditions

Guard conditions can continue (mimicking the current use of where), break, return, or otherwise exit scope. This offers more flexible behavior.

for x in sequence {
    guard condition else { continue } // current where behavior
    guard condition else { break } 
    guard condition else { return } 
    guard condition else { fatalError() }, // etc.
}

Removing where from for-in loops reduces cognitive burden when interpreting intent. The logic is easier to read and follow. And the Swift grammar is simpler.

Malformed Grammar

In Swift's current form, the where-clause in for-loops inconsistently applied. Unlike switch statements and do loops, a for-in loop's where-clause is separated from the pattern it modifies.

for case? pattern in expression where-clause? code-block

case-item-list → pattern where-clause? | pattern where-clause? , case-item-list

catch pattern? where-clause? code-block

This separation makes the clause harder to associate with the pattern, can confuse users as to whether it modifies the expression or the pattern, and represents an inconsistency in Swift's grammar. The where-clause really should have been designed like this:

for case? pattern where-clause? in expression code-block

Other Where Clause Uses

This proposal does not affect where clause use in generics. Using generic constraints unamibiguously offers positive utility.

Retiring where from catch clauses and switch statements is less clear cut.

case_item_list : pattern where_clause? | pattern where_clause? ',' case_item_list
catch_clause : 'catch' pattern? where_clause? code_block
Case:
  • Instances of case.*: in the standard library: 1337 (!)
  • Instances of case.*where.*: in the standard library: 1-ish
  • Instances of case.*: in my Apple sample code collection: 40 (!)
  • Instances of case.*where.*: in my Apple sample code collection: 7
  • Instances of case.*: in popular 3rd party source code: Over 1400
  • Instances of case.*where.*: in popular 3rd party source code: 17
public/core/String.swift:        // case let x where (x >= 0x41 && x <= 0x5a):
Catch:
  • Instances of catch in popular 3rd party source code: 75
  • Instances of catch.*where in popular 3rd party source code: 0
  • Instances of catch in the standard library: 18
  • Instances of catch.*where in the standard library: 0

Unlike generic constraints, nothing prevents semantic disjunction in switch-case and catch where clauses, both provide expressive potential that could be missed.

Detailed Design

This proposal removes the where clause from the for-in loop grammar:

for case? pattern in expression code-block

Impact on Existing Code

Code must be refactored to move the where clause into guard (or, for less stylish coders, if) conditions.

Alternatives Considered

  • Not accepting this proposal, leaving the grammar intact.

  • Including catch and case under the umbrella of this proposal. I think the general Swift user base would be extremely upset. Redesigning switch and catch statements to allow disjoint expressions a la SE-0099 would be difficult and disruptive.

  • Change where in catch and case clauses to if, restricting where clauses strictly to type constraints without burning a new keyword. As Xiaodi Wu puts it, "Replacing where with if is unambiguous and eliminates the implication of a subordinate semantic relationship that can't be enforced, while still exposing all of the expressiveness made possible by where in that particular scenario."

    switch json {
    case let json as NSArray if json.count > 0:
        // handle non-empty array
    case let json as NSDictionary if json.allKeys.count > 0:
        // handle non-empty dict
    default:
        break
    }

Acknowledgements

Big thanks to Joe Groff, Brent Royal-Gordon, Xiaodi Wu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment