SE-0105: Removing Where Clauses from For-In Loops
* Proposal: [SE-0105](0105-remove-where-from-forin-loops.md) * Author: [Erica Sadun](https://github.com/erica) * Review Manager: [Chris Lattner](https://github.com/lattner) * Status: **Rejected** * Decision Notes: [Rationale](https://forums.swift.org/t/rejected-se-0105-removing-where-clauses-from-for-in-loops/3205)
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](https://forums.swift.org/t/pitch-retiring-where-from-for-in-loops/2926)
Motivation
As syntactic sugar, the for loop's where clause is rarely used, hard to discover, and elevates one style (continue on condition, aka filtering) above other related styles: break on condition (while or until), return on condition (unless), throw on condition, and abort (fatalError()) on condition. The where clause supports a fluent style that is difficult to document separately at its point of use and may be hard to breakpoint and debug. Eliminating where in favor of guard statements addresses all these points: better commenting, better breakpointing and debugging, and full domain coverage over filtering and early exit in a way that where cannot.
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.
Completeness of Guard Conditions
Guard conditions can continue (mimicking the current use of where), break, return, or otherwise exit scope. This offers more flexible and complete behavior.
for x in sequence {
guard condition else { continue } // current where behavior
guard condition else { break }
guard condition else { return }
guard condition else { throw error }
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.
SE-0099
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."
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-blockThis 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-blockOther 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_blockCase:
- 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
catchin popular 3rd party source code: 75 - Instances of
catch.*wherein popular 3rd party source code: 0 - Instances of
catchin the standard library: 18 - Instances of
catch.*wherein 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-blockImpact 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
catchandcaseunder the umbrella of this proposal. I think the general Swift user base would be extremely upset. Redesigningswitchandcatchstatements to allow disjoint expressions a la SE-0099 would be difficult and disruptive.
- Change
whereincatchandcaseclauses toif, restrictingwhereclauses strictly to type constraints without burning a new keyword. As Xiaodi Wu puts it, "Replacingwherewithifis unambiguous and eliminates the implication of a subordinate semantic relationship that can't be enforced, while still exposing all of the expressiveness made possible bywherein 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
}- Extending the syntactic sugar in
for-inloops to includewhile,unless, anduntil. This adds all four variations onbreakandcontinueto thefor-invocabulary, and might include a simultaneous renaming ofwheretoif.
Acknowledgements
Big thanks to Joe Groff, Becca Royal-Gordon, Xiaodi Wu