Trailing commas in all expression lists

Trailing commas in all expression lists

Introduction

Currently, you can use trailing commas when writing array and dictionary literals. Using this feature, you can conveniently toggle any expression's inclusion in your collection literal just by commenting and uncommenting the line the expression appears on. This document proposes bringing that convenience to all expression lists.

Motivation

Today, you can easily add or remove any item, even the last, from a collection literal by commenting it in or out:


let colors = [
    "red",
    "green",
    "blue",
//    "cerulean"
]

Unfortunately, in Swift today, that convenience is not fully available in the other expression lists.

For example, in a multi-line function call, it is a breeze to comment out any argument

print(
    "red",
//    "green", // ok
    "blue",
    "cerulean"
)
  • except * the last; commenting it out raises an error:
print(
    "red",
    "green",
    "blue", // error: unexpected ',' separator
//    "cerulean"
)

The problem is that current Swift does not accept a comma after the last expression in an argument list.

It is possible to work around that limitation by adopting a different whitespace style

print(
    "red"
  , "green"
  , "blue" // no error: the comma is now at the beginning of the next line
//   , "cerulean"
)

but doing so has several downsides:

  • that style may be inconsistent with the formatting of collection literals since those do not need to work around the same limitation
  • formatting tools may not produce the desired indentation
  • that style is not widely adopted

Moreover, this is just a workaround. Developers who don't adopt this particular whitespace style won't be afforded the convenience of uniformly commenting their function arguments in and out.

Rather than forcing developers to use a particular whitespace style, Swift should consistently allow every argument passed to a multi-line function to be commented in and out.

To afford developers this convenience analogous contexts, Swift should consistently accept a single trailing comma in non-empty every expression list.

Proposed solution

Rather than allowing trailing commas in more expression lists in an ad hoc fashion, this document proposes uniformly allowing trailing commas in all non-empty expression lists:

Allowing trailing commas in these positions provides the convenience already available in collection literal expressions lists to all expression lists. Every expression--including the last--would be able to be included/excluded from the expression list just by commenting out the line(s) it appears on.

Moreover, accepting trailing commas in these positions makes the language consistent. Currently, trailing commas are accepted only in two particular expression lists, namely when the expression list is a collection literal. With the change, trailing commas would consistently be accepted in every expression list.

With this change, trailing commas will be accepted in the following positions:

  • array literals (already allowed)
["red", "green",]
  • dictionary literals (already allowed)
["red" : 4, "green" : 8,]
  • object literals
#colorLiteral(red: 0.0, green: 0.482, blue: 0.655, alpha: 1.0,)
  • free function calls
print("red", "green",)
  • method calls
foo.print("red", "green",)
  • initializer calls
let instance = Widget("red", "green",)
  • subscript reads
foo["red", "green",]
  • subscript writes
foo["red", "green",] = "yellow"
  • super method calls
super.print("red", "green",)
  • super initializer calls
super.init("red", "green",)
  • super subscript reads
super["red", "green",]
  • super subscript writes
super["red", "green",] = "yellow"
  • enum instantiations
let e = E.foo("red", "green",)
  • tuple instantiations
let t = ("red", "green",)
  • key-path subscripts
let path = \Gadget[0, 1,]

Trailing commas will not, however, be accepted in empty expression lists. Text like the following will not parse:

let arrr: [Int] = [,] // error: unexpected ',' separator
let subs = arrr[,] // error: expected expression in container literal
print(,) // error: unexpected ',' separator
let empT = (,) // error: unexpected ',' separator

Detailed design

Swift will accept an optional trailing comma in every non-empty expression list.

The grammatical productions from The Swift Programming Language will be modified as follows:

expression-list -> expression ,opt | expression , expression-list
function-call-argument-list -> function-call-argument ,opt | function-call-argument , function-call-argument-list
tuple-element-list -> tuple-element ,opt | tuple-element , tuple-element-list
playground-literal -> #colorLiteral(red : expression, green : expression, blue : expression, alpha : expression ,opt)
playground-literal -> #fileLiteral(resourceName : expression ,opt)
playground-literal -> #imageLiteral(resourceName : expression ,opt)

With these few changes to the grammatical productions, trailing commas will be accepted in all the places described in the previous section.

Future directions

This document proposes changing the language to accept trailing commas in every expression list. There is a reasonable further easing of the restriction on trailing commas: allow trailing commas in every comma-separated list.

The same observations about language consistency and developer convenience both apply to eliminating the restriction entirely. The observations don't, however, carry the same force in that broader context.

Swift currently has a minor inconsistency in that it accepts trailing commas in only some expression lists. The change proposed here would makes the language consistent: Swift would accept trailing commas in every non-empty expression list. The further change to allow trailing commas in every comma-separated list would also make the language consistent; it is not, however, the minimal change which would make the language consistent. The change in this proposal is.

Regarding convenience, by far the most common comma-separated lists are expression lists, especially function calls. Consequently, given the smaller change outlined in this proposal, the further easing of the restriction on trailing commas to allow them in every comma-separated list would provide significantly less additional advantage than the smaller language change would provide over the current state of affairs.

For those reasons, that larger change is deferred. This proposal does, however, leave open the door for accepting trailing commas in every comma-separated list.

Source compatibility

N/A

Effect on ABI stability

N/A

Effect on API resilience

N/A

Alternatives considered

  • Allow trailing commas in function calls, function declarations, tuple type definitions, and tuple literals (SE-0084).

SE-0084 was rejected.

  • Only allow trailing commas in expression lists when the surrounding whitespace satisfies some condition. For example, allow a trailing comma whenever it is separated by a newline from the list closing token (parenthesis or bracket).

Rationale: The grammar generally avoids legislating style.

  • Using newlines rather than commas to delimit multi-line expression lists.

Rationale: Doing so would lead to ambiguities between member accesses and implicit member expressions such as

[
    foo()
    .bar
]
24 Likes

Sounds great to me. Type argument lists Foo<A,B,C,> are also arguably expression grammar.

7 Likes

I'm a fan! It would make working with DSLs a lot nicer.

+1 for the proposal from me as well, and another +1 to extending it to type argument lists (which I would like to see be as consistent with function arguments as much as possible, specifically with support for labels and default arguments).

1 Like

I can testify that the current rules are a hassle for generated code. I've been working on a project using SwiftSyntax; when it needs to generate a list with commas, it currently generates all of the syntax nodes without commas and then does a second pass through the list to add them. This is inefficient, but it's simpler than generating the commas correctly in the first place, and it's still a couple dozen lines of tedium.

I might even consider extending this further than expression lists. There are a few places in statements where we use comma separated "lists" which aren't enclosed in brackets:

  • Multi-clause if and guard statements
if let x = y, !x.isEmpty, { ... }
  • Tuple patterns
let (x, y,) = tupleFn()
  • case statements with multiple patterns
case .x, .y, :

We might also think about extending it to declarations:

  • Attribute and modifier argument lists (this one especially, since they're designed to feel like function calls)
@available(iOS, deprecated: 10.0,)
  • Parameter declaration lists (for functions, initializers, subscripts)
func foo(x: Int, y: Double, ) { ... }
  • Generic parameter declaration lists (for types, functions, initializers, subscripts)
struct Foo<X, Y, > { ... }
  • Generic where clauses
func foo<X>(x: X) where X: BidirectionalCollection, X: MutableCollection, { ... }
5 Likes

Agreed, I've been caught by this. Also +1 for type argument lists.

+1, Yes, please. Nice little quality of life improvement right there.

+1 because duh :grin:

Thanks everyone for the initial feedback!

It seems like, at the moment, there are two questions to resolve:

  1. @Joe_Groff Are type argument lists part of the expression grammar? (You said "arguably".)

If they are, then that probably needs to be part of any change calling itself "trailing commas in all expression lists".

  1. Can we defer accepting trailing commas in all comma-separated lists (as mentioned in the "Future directions" section and as raised by @beccadax)?

One argument in favor of deferring accepting trailing commas in all comma-separated lists begins with the observation that the most common by far comma-separated lists in Swift are expression lists. The language change to accept them in those positions is smaller (than the change to accept them in all positions) and covers the most common cases. If the ideal is allowing trailing commas in all comma-separated lists, then accepting them in all expression lists gets us most of the way there, with a smaller language change. We may be in an 80/20 situation where the smaller language change gets us most of the way to where we would like to eventually be and the larger language change only gets us a bit farther.

That's not an argument against ever going farther, but an argument in favor of a gradual easing of the restrictions on trailing commas, relaxing them first at the greatest pain points and relaxing them in other positions later.

Additionally, the smaller change isn't piecemeal but leaves the language in a more consistent state.

Is this (or another!) argument convincing?

As is mentioned in the Alternatives considered section, the previous proposal SE-0084 was discussed and ultimately Rejected. For reference, here was the Rationale:

The feedback from the community was quite divided on this topic: many people contributed to the discussion thread with some people agreeing and some disagreeing.
Swift currently accepts a trailing comma in array and dictionary collection literals, for three reasons: evolution of a project often adds and removes elements to the collection over time, these changes do not alter the type of the collection (so those changes are typically spot changes), and the closing sigil of the collection (a right square bracket) is often placed on the line following the elements of the collection. Because of these properties, accepting a trailing comma in a collection literal can help reduce spurious diffs when elements are added or removed.
That said, these properties do not translate to other comma separated lists in Swift, such as variable bindings in a var/let declaration, parameter lists or tuples. For parameter lists and tuples (the specific topic of the proposal), the trailing terminator of the list is typically placed on the same line as the other elements. Further, changes to add or remove an element to a parameter list or tuple element list change the type of the tuple or the signature of the call, meaning that there is almost always changes in other parts of the code to allow the change to build. Finally, the core team does not want to encourage or endorse a coding style that puts the terminating right parenthesis on a line following the arguments to that call.

Except from the fact that the current pitch is slightly different as it allows trailing commas in all expression lists, is there anything about it that voids the decision previously reached?

3 Likes

I don't want to come across as negative against the effort that was already spent writing the proposal and working on an implementation (!), but I'm just not sure if it's very helpful to have another divisive debate on what is essentially the same proposal as SE-0084.

A lot's happened since then, and we have a pretty solid set of feedback that that was a mistake. This change is small and backward compatible, and I don't see a problem reconsidering it.

13 Likes

It seems like a worthwhile exercise to at least go through all the places in the grammar where we could add a trailing comma and have the grammar work. If there aren't any grammatical or implementation issues with doing so, I'm not sure we win anything from doing this piecemeal over many proposals instead of doing it all at once. If it's in fact considerably easier for you as implementer to only handle expressions, though, I can understand why you'd want to limit the scope.

2 Likes

The comma-handling that you get in a generic argument clause is separate from the general expression list grammar.

Both the source comment grammar and the grammar in TSPL treat these lists separately from expression lists.

For the purposes of this proposal, it seems like it'd be easier if you explicitly called this case out if you want it.

Yeah, technically it's a separate grammar, but types can nonetheless appear as subterms within expressions, so it may be surprising to a user if trailing commas don't work in them if they work in all other expression contexts.

It seems like the subterm-within-expressions umbrella is so large that people probably aren't reasoning about it like that. E.g.:

print({ () -> Void in struct Foo<A, B, C> {} })

That's all an expression, but a programmer probably isn't thinking about what's allowable in an expression when typing the deeply-nested generic parameters there.

Anyhow, I think we're on the same page in terms of wanting consistent ergonomics for comma-separated lists.

Types can appear directly inside expressions, like Foo<T, U, V>.bar(x, y, z). With closures, sure you can transitively include the entire grammar, but more indirectly, and at least to me, that's understandable by their nature.

Can you share the feedback you received that resulted in this change of mind? Iā€™m genuinely interested.

2 Likes

What about in multi-expression guards? This is, for me, the absolute biggest pain point scenario where I always want trailing comma support.

8 Likes

+1. I also like this because in cases where an item is removed from the end of an expression list, the diff contains only the line that was changed (and not an additional line for a comma removal).

3 Likes