|
| 1 | +# Allow trailing comma in comma-separated lists |
| 2 | + |
| 3 | +- Proposal: [SE-0439](0439-trailing-comma-lists.md) |
| 4 | +- Author: [Mateus Rodrigues](https://github.com/mateusrodriguesxyz) |
| 5 | +- Review Manager: [Xiaodi Wu](https://github.com/xwu) |
| 6 | +- Status: **Active review (July 1...July 14, 2024)** |
| 7 | +- Implementation: https://github.com/swiftlang/swift/pull/74522# gated behind `-enable-experimental-feature TrailingComma` |
| 8 | +- Review: [pitch](https://forums.swift.org/t/pitch-allow-trailing-comma-in-tuples-arguments-and-if-guard-while-conditions/70170) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +This proposal aims to allow the use of trailing commas, currently restricted to array and dictionary literals, in comma-separated lists whenever there are terminators that enable unambiguous parsing. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +### Development Quality of Life Improvement |
| 17 | + |
| 18 | +A trailing comma is an optional comma after the last item in a list of elements: |
| 19 | + |
| 20 | +```swift |
| 21 | +let rank = [ |
| 22 | + "Player 1", |
| 23 | + "Player 3", |
| 24 | + "Player 2", |
| 25 | +] |
| 26 | +``` |
| 27 | + |
| 28 | +Swift's support for trailing commas in array and dictionary literals makes it as easy to append, remove, reorder, or comment out the last element as any other element. |
| 29 | + |
| 30 | +Other comma-separated lists in the language could also benefit from the flexibility enabled by trailing commas. Consider the function [`split(separator:maxSplits:omittingEmptySubsequences:)`](https://swiftpackageindex.com/apple/swift-algorithms/1.2.0/documentation/algorithms/swift/lazysequenceprotocol/split(separator:maxsplits:omittingemptysubsequences:)-4q4x8) from the [Algorithms](https://github.com/apple/swift-algorithms) package, which has a few parameters with default values. |
| 31 | + |
| 32 | + |
| 33 | +```swift |
| 34 | +let numbers = [1, 2, 0, 3, 4, 0, 0, 5] |
| 35 | + |
| 36 | +let subsequences = numbers.split( |
| 37 | + separator: 0, |
| 38 | +// maxSplits: 1 |
| 39 | +) ❌ Unexpected ',' separator |
| 40 | +``` |
| 41 | + |
| 42 | +### The Language Evolved |
| 43 | + |
| 44 | +Back in 2016, a similar [proposal](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0084-trailing-commas.md) with a narrower scope was reviewed and rejected for Swift 3. Since that time, the language has evolved substantially that challenges the basis for rejection. The code style that "puts the terminating right parenthesis on a line following the arguments to that call" has been widely adopted by community, Swift standard library codebase, swift-format, docc documentation and Xcode. Therefore, not encouraging or endorsing this code style doesn't hold true anymore. |
| 45 | + |
| 46 | +The language has also seen the introduction of [parameter packs](https://github.com/apple/swift-evolution/blob/main/proposals/0393-parameter-packs.md), which enables APIs that are generic over variable numbers of type parameters, and code generation tools like plugins and macros that, with trailing comma support, wouldn't have to worry about a special condition for the last element when generating comma-separated lists. |
| 47 | + |
| 48 | +## Proposed solution |
| 49 | + |
| 50 | +This proposal adds support for trailing commas in comma-separated lists when there's a clear terminator, which are the following: |
| 51 | + |
| 52 | +- Tuples and tuple patterns. |
| 53 | + |
| 54 | +```swift |
| 55 | +(1, 2,) |
| 56 | +let block: (Int, Int,) -> Void = { (a, b,) in } |
| 57 | +let (a, b,) = (1, 2,) |
| 58 | +for (a, b,) in zip(s1, s2) { } |
| 59 | +``` |
| 60 | + |
| 61 | +- Parameter and argument lists of initializers, functions, enum associated values, expression macros, attributes, and availability specs. |
| 62 | + |
| 63 | +```swift |
| 64 | + |
| 65 | +func foo(a: Int, b: Int,) { } |
| 66 | + |
| 67 | +foo(a: 1, b: 1,) |
| 68 | + |
| 69 | +struct S { |
| 70 | + init(a: Int, b: Int,) { } |
| 71 | +} |
| 72 | + |
| 73 | +enum E { |
| 74 | + case foo(a: Int, b: Int,) |
| 75 | +} |
| 76 | + |
| 77 | +@Foo(1, 2, 3,) |
| 78 | +struct S { } |
| 79 | + |
| 80 | +f(_: @foo(1, 2,) Int) |
| 81 | + |
| 82 | +#foo(1, 2,) |
| 83 | + |
| 84 | +struct S { |
| 85 | + #foo(1, 2,) |
| 86 | +} |
| 87 | + |
| 88 | +if #unavailable(iOS 15, watchOS 9,) { } |
| 89 | + |
| 90 | +``` |
| 91 | +- Subscripts, including key path subscripts. |
| 92 | + |
| 93 | +```swift |
| 94 | +let value = m[x, y,] |
| 95 | + |
| 96 | +let keyPath = \Foo.bar[x,y,] |
| 97 | + |
| 98 | +f(\.[x,y,]) |
| 99 | +``` |
| 100 | + |
| 101 | +- `if`, `guard` and `while` condition lists. |
| 102 | + |
| 103 | +```swift |
| 104 | +if a, b, { } |
| 105 | +while a, b, { } |
| 106 | +guard a, b, else { } |
| 107 | +``` |
| 108 | + |
| 109 | +- `switch` case labels. |
| 110 | + |
| 111 | +```swift |
| 112 | +switch number { |
| 113 | + case 1, 2,: |
| 114 | + ... |
| 115 | + default: |
| 116 | + .. |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +- Closure capture lists. |
| 121 | + |
| 122 | +```swift |
| 123 | +{ [a, b,] in } |
| 124 | +``` |
| 125 | + |
| 126 | +- Inheritance clauses. |
| 127 | + |
| 128 | +```swift |
| 129 | +struct S: P1, P2, P3, { } |
| 130 | +``` |
| 131 | + |
| 132 | +- Generic parameters. |
| 133 | + |
| 134 | +```swift |
| 135 | +struct S<T1, T2, T3,> { } |
| 136 | +``` |
| 137 | + |
| 138 | +- Generic `where` clauses. |
| 139 | + |
| 140 | +```swift |
| 141 | +struct S<T1, T2, T3> where T1: P1, T2: P2, { } |
| 142 | +``` |
| 143 | + |
| 144 | +- String interpolation |
| 145 | + |
| 146 | +```swift |
| 147 | +let s = "\(1, 2,)" |
| 148 | +``` |
| 149 | + |
| 150 | +## Detailed Design |
| 151 | + |
| 152 | +Trailing commas will be supported in comma-separated lists whenever there is a terminator clear enough that the parser can determine the end of the list. The terminator can be the symbols like `)`, `]`, `>`, `{` and `:`, a keyword like `where` or a pattern code like the body of a `if`, `guard` and `while` statement. |
| 153 | + |
| 154 | +Note that the requirement for a terminator means that the following cases will not support trailing comma: |
| 155 | + |
| 156 | +Enum case label lists: |
| 157 | + |
| 158 | +```swift |
| 159 | +enum E { |
| 160 | + case a, b, c, // ❌ Expected identifier after comma in enum 'case' declaration |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +Inheritance clauses for associated types in a protocol declaration: |
| 165 | + |
| 166 | +```swift |
| 167 | +protocol Foo { |
| 168 | + associatedtype T: P1, P2, // ❌ Expected type |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +Generic `where` clauses for initializers and functions in a protocol declaration: |
| 173 | + |
| 174 | +```swift |
| 175 | +protocol Foo { |
| 176 | + func f<T1, T2>(a: T1, b: T2) where T1: P1, T2: P2, // ❌ Expected type |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +Trailing commas will be allowed in single-element lists but not in zero-element lists, since the trailing comma is actually attached to the last element. Supporting a zero-element list would require supporting _leading_ commas, which isn't what this proposal is about. |
| 181 | + |
| 182 | +```swift |
| 183 | +(1,) // OK |
| 184 | +(,) // ❌ expected value in tuple |
| 185 | +``` |
| 186 | + |
| 187 | + |
| 188 | +## Source compatibility |
| 189 | + |
| 190 | +Although this change won't impact existing valid code it will change how some invalid code is parsed. Consider the following: |
| 191 | + |
| 192 | +```swift |
| 193 | +if |
| 194 | + condition1, |
| 195 | + condition2, |
| 196 | +{ // ❌ Function produces expected type 'Bool'; did you mean to call it with '()'? |
| 197 | + return true |
| 198 | +} |
| 199 | + |
| 200 | +{ print("something") } |
| 201 | +``` |
| 202 | + |
| 203 | +Currently the parser uses the last comma to determine that whatever follows is the last condition, so `{ return true }` is a condition and `{ print("something") }` is the `if` body. |
| 204 | + |
| 205 | +With trailing comma support, the parser will terminate the condition list before the first block that is a valid `if` body, so `{ return true }` will be parsed as the `if` body and `{ print("something") }` will be parsed as an unused closure expression. |
| 206 | + |
| 207 | +```swift |
| 208 | +if |
| 209 | + condition1, |
| 210 | + condition2, |
| 211 | +{ |
| 212 | + return true |
| 213 | +} |
| 214 | + |
| 215 | +{ print("something") } // ❌ Closure expression is unused |
| 216 | +``` |
| 217 | + |
| 218 | +## Alternatives considered |
| 219 | + |
| 220 | +### Eliding commas |
| 221 | + |
| 222 | +A different approach to address similar motivations is to allow the comma between two expressions to be elided when they are separated by a newline. |
| 223 | + |
| 224 | +```swift |
| 225 | +print( |
| 226 | + "red" |
| 227 | + "green" |
| 228 | + "blue" |
| 229 | +) |
| 230 | +``` |
| 231 | +This was even [proposed](https://forums.swift.org/t/se-0257-eliding-commas-from-multiline-expression-lists/22889/188) and returned for revision back in 2019. |
| 232 | + |
| 233 | +The two approaches are not mutually exclusive. There remain unresolved questions about how the language can accommodate elided commas, and adopting this proposal does not prevent that approach from being considered in the future. |
0 commit comments