[Pitch] Allow trailing comma in tuples, arguments and if/guard/while conditions

I've put together a pitch text to extend the use of trailing comma. Unfortunately, I don't have the skills to implement it myself but if anyone is willing to do at least there's an updated text partially written for the proposal.

Allow trailing comma in tuples, arguments/parameters and if/guard/while conditions

  • Proposal: SE-NNNN
  • Author: Mateus Rodrigues
  • Review Manager: TBD
  • Status: Awaiting Review
  • Implementation: swift, swift-syntax (gated behind -enable-experimental-feature TrailingComma).
  • Review: (pitch)

Introduction

This proposal aims to allow the use of trailing commas, currently restricted to array and dictionary literals, in tuples, arguments/parameters and if/guard/while conditions.

Motivation

Development Quality of Life Improvement

A trailing comma is a optional comma after the last item in a series of elements:

let rank = [
  "Player 1",
  "Player 3",
  "Player 2",
]

Using trailing commas makes it easy to add, remove, reorder and comment in/out elements, without the need to add or delete the comma while doing any of these manipulations.

Consider the following SwiftUI modifier:

func frame(
    width: CGFloat? = nil,
    height: CGFloat? = nil,
    alignment: Alignment = .center
) -> some View

frame(width:), frame(height:), frame(width:alignment:), frame(height:alignment:), frame(width:height:), frame(width:height:alignment:) are all valid calls but you can't easily swipe between frame(width:) and frame(width:alignment:) by commenting in/out alignment without add/remove trailing comma.

.frame(
    width: 500,
//    alignment: .leading
) ❌ Unexpected ',' separator

The introduction of parameter packs allows more APIs that are list-like at call site and would benefit from trailing comma.

extension [S] {
    func sorted<each T: Comparable>(_ keyPath: repeat KeyPath<S, each T>) { }
}

arrayOfS.sorted(
  \.a, 
  \.b,
//  \.c
) ❌ Unexpected ',' separator

Since #21381 has been merged back in 2019 enum associated values supports default values and are a good fit for trailing comma as well.

Tuples use are very close to parameter list and, although may not be so frequently used, it seems natural that they adopt trailing comma too.

Multiple conditions in if, guard and while are also list-like and add, remove, reorder and comment in/out are not uncommon practice during development.

if 
   condition1,
   condition2,
//   condition3
{ ❌ Cannot convert value of type '() -> ()' to expected condition type 'Bool'
                    
} ❌ Expected '{' after 'if' condition

Code Generation

Plugins and Macros have made it possible to generate code using swift and trailing comma would allow generate list of parameters and conditions without worrying about a special condition for the last element.

Code Diff

A tangential motivation is that trailing comma makes version-control diffs cleaner.

Without trailing comma:

foo(
-  a: Int
+  a: Int,
+  b: Int
)

With trailing comma:

foo(
  a: Int,
+  b: Int,
)

[!NOTE]
A similar proposal was rejected back in 2016 for Swift 3. It's been 8 years since that, the swift language has evolved a lot, some changes highlighted above as motivation, and 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 encourage or endorse this code style doesn't hold true anymore nor is a reason for rejection.

Proposed solution

This proposal adds support for trailing comma to:

Arguments/Parameters

Including declaration and call of initializers, functions and enum case associated values.


func foo(
    a: Int = 0, 
    b: Int = 0, 
) {
}

foo(
    a: 1,
    b: 2,
)

Tuples

(1, 2, 3,)

Conditions

Including if, guard and while.

if 
   condition1,
   condition2,
   condition3,
{
                    
}

Source compatibility

This change is purely additive and has no impact on existing code.

Future directions

Allow trailing comma anywhere there's a comma-separated list

Although this proposal focuses on the most requested use cases for trailing comma, there's other places with comma-separated list and the restriction could be consistently lifted for all of these.

Subclass and Protocol Conformance

class C2: C1, P1, P2, { } 

Generics

struct S<T1, T2,> where T1: P2, T2: P2, { }

Switch Case

switch number {
    case 1, 2, 3,:
        ...
    default:
        ...
}

Alternatives considered

Eliding commas from multiline expression lists

A different approach to address the exact same motivation is to allow the comma between two expressions to be elided when they are separated by a newline.

print(
    "red"
    "green"
    "blue"
)

This was even proposed and returned to revision back in 2019.

Even though both approach are not mutually exclusive, this proposal is about consistently extend an existing behavior in the language while eliding comma is a more serious change to the language.

47 Likes

A reminder that another proposal with a different approach was reviewed in SE-0257 and returned for revision; the review process was polarizing and generally...not good, and the reason for the proposal being returned by the core team was that concerns during the pitch had not been settled sufficiently for review.

1 Like

I don't personally use this style, where the closing paren sits on its own line:

foo(
  a: Int,
  a: Int
)

I know the alternative style where the closing paren sits on the same line as the last argument is also very popular. This is what I use and what I personally see most often in my sphere:

foo(
  a: Int,
  a: Int)

The style discussed in this proposal is of course very common, and deserves to be supported by the language as best as possible, even if it isn't completely universal.

I think allowing a trailing comma here, like in collection literals, would be a great addition that improves the consistency of the language and removes a small amount of friction, with basically no downsides.

(ok, one downside -- foo(a: 1, b: 2,) is kinda funny looking with a trailing comma. but, this seems mostly harmless since it's allowed today in array literals like [1, 2,] and it's also trivial to lint away if you dislike it).

This proposal is very different! It seems basically unprecedented, compared to this proposal which has a solid precedent elsewhere in the language.

9 Likes

Definitely a +1 for me, something I often wish for.
I believe the arguments with macros and parameter packs have especially made a difference, well enough for this to be re-pitched.

9 Likes

Very different (in fact, diametrically opposite) solution addressing the exact same motivation—i.e., part of the design space and each necessarily an alternative to be considered for the other. And certainly not unprecedented: it presaged result builders and is of-a-kind with discussions on eliding return.

Anecdotally, from talking with many developers, there's been a lot of regret that we rejected the first trailing comma proposal. And speaking only for myself, I think it's a good idea to consistently allow trailing commas anywhere we have a comma-separated list in the grammar, as long as there is some surrounding delimiters such as parens, brackets, or a following keyword to ensure the end of the list remains unambiguous. So I'd like to see us revisit this decision.

55 Likes

I would welcome this. It would be more git-friendly for code review.

7 Likes

In particular, it would make updates to targets in Package.swift much nicer.

4 Likes

Of course, if trailing commas get support but eliding commas doesn't, many people will be very upset just by that 'injustice'. For better or worse.

Omitting commas is logically a superset - if you don't have commas, you don't need trailing commas. It's also better-precedented in the language - e.g. semicolons may already be omitted - and follows a core Swift tenet of removing unnecessary ceremony.

So if the community (or Swift team) couldn't agree on eliding commas, then they're not going to come to agreement on trailing commas.

2 Likes

I don’t think that follows, purely because lots of languages allow trailing commas and elided semicolons, but not elided commas. That makes it a less controversial change even if they share some characteristics.

General elision also adds more room for ambiguity, whereas all of the positions where a trailing comma is being proposed here have a known terminator character () in parameter lists and tuples, { for condition lists).

I am in favor of this, though I think it also reignites (my) desires for a standard formatter.

31 Likes

To keep the text updated with the discussion I have added a Alternatives considered section acknowledging comma elision and also mentioning other comma-separated lists.

8 Likes

Would these changes also carry through to Package.swift? By far one of the places I’ve spent the most time both changing lists like this, and debugging missing commas from said lists.

Overall, I think this proposal is a solid quality of life improvement with little downside.

3 Likes

I did stress logically a superset. Indeed it is more common to see trailing commas supported but eliding commas not. I put that down to simple conservatism - trailing commas have a long history whereas eliding them is still relatively new.

Just like omitting semicolons was, until relatively recently.

Maybe eliding commas has more potential for ambiguity - I haven't really looked thoroughly, nor seen anyone else do so - but trailing commas aren't without concerns either. Trailing commas are inherently ambiguous because you can't be certain (on face value) if the trailing comma truly is superfluous or is an error whereby the next element is missing.

I'm focused on the human experience, in particular, not so much the compiler. Compilers are allowed to have a harder time correctly interpreting things, because they only have to be written once and they're more consistent.

If you have the mindset that git diff noise is irrelevant in this respect, trailing commas are only detrimental because they introduce ambiguity (and extra typing) for no benefit¹.

Whereas eliding commas at least saves you some typing, some reading, and makes reordering items simpler in some cases (and simplifies your git diffs, for those that care).

To be clear, I have no problem with trailing commas being permitted - to each their own. I just think it's premature to pursue that until the question of whether or not to allow eliding commas is properly settled. That the first such proposal was rejected doesn't settle it for me - it appears it was rejected largely (if not entirely) because of some ugliness within the community during the pitch discussion & review, not the proposal's actual merits.


¹ I've heard it said that it's faster to rearrange list items if there's always a comma after every item, but I'm not convinced that's (a) true, (b) important, and (c) commonly applicable anyway, given it's incompatible with multiple items on one line, preceding or trailing elements like ellipses and brackets, etc.

2 Likes

We cannot compatibly elide commas, and we shouldn't hold up the discussion about trailing commas on that ability IMO. One counterexample is that

foo(bar
  .bas())

currently parses as a method call bar.bas(), in a single-argument function call to foo, whereas

foo(bar,
  .bas())

parses as two arguments.

22 Likes

I'll agree with some folks above that, as someone who was strongly against allowing trailing commas in the original round of review of SE-0084, I've come around and changed my opinion on the matter. As others have mentioned, Swift being used to implement both logic but also be used as a DSL is one factor—just this past weekend I was annoyed shuffling things in a Package.swift file around and having to deal with the commas. It's not clear whether we'd switch SPM over to a result-builder-style approach any time soon, either. Macros are the other motivating factor; if we want users to be able to write them well, forcing them to audit trailing commas in their lists is just a frustration without purpose.

I can imagine no world where I would want to see trailing commas elided. Let's not discuss it further (in this thread) and risk dragging down a real positive change here.

30 Likes

The one construct where I really miss trailing commas is the guard statement. While I might use them in function argument lists, those tend to be more static. One is less likely to need to add or remove a parameter to an invocation after it is has been written. However, with guard, I often find I need to add or remove a condition.

7 Likes

I remember the first time coming across the concept of eliding punctuation to make refactoring/reorganizing code easier. It was when I was taking COBOL in university and my father, a long-time COBOL programmer, told me I could elide the period at the end of each statement.

I have neither written COBOL nor considered eliding trailing punctuation since that time.

this would be less pressing for me if sourcekit-lsp did not completely fritz out when a guard statement contains a trailing comma. i do feel like a minor change in the way swift-syntax handles extraneous trailing commas could go a long way towards mitigating this annoyance.

1 Like

I see the tides are in favor, but to be fair...

I would optimize for reading. To me a trailing comma reads as an error, unless it is in a lineated list/array literal. It stops my visual scan while I consider whether it is correct. If this changes, we'll have to live with that little hiccup in all the code we read. We easily read 100-1000 lines for every line changed.

I agree it can be awkward to edit, review, or produce, but those are one-time operations.

This change introduces an optional/choice/format alternative, which makes for inconsistency in code bases. (I would have made trailing commas a requirement in lineated list literals when the trailing delimiter is not on the same line.) swift-format has to change to permit commas, but it also has to decide whether to add the comma when converting from inline to lineated (and remove when reversing?). Then developers have to decide whether to reformat to add trailing commas for consistency. There's no way for swift-format to discover code conventions, so one or the other has to be imposed globally (restricted to changed lines only?). (Unclear what should happen when comments are after the comma, or even before.)

If trailing commas are approved, I would start only with the high-traffic cases:

  1. if and guards, because they are long and commas-as-conjunction are a Swifty thing anyway
  2. Parameter lists (both declaration and call) because they're often so long

These are distinguished as often composing complex expressions, where the visual-scan rate is lower anyway.

If I could, I would restrict it to cases without delimiting parentheses on the same line.

As for other cases, I see little motivation and lots of confusion with trailing commas in tuples or type lists (or enum cases) (and parameter packs have little traffic and lots of confusion already). I can see adopting a swift-format rule imposing trailing commas on lineated conditions and parameter lists, but not on type lists or tuples. Type lists are relatively rarely written or changed.

However, tuples are used a lot in evolving API, gaining new members until they become real struct's, so they're higher-traffic both in read and in edit. They're less likely to have complex atoms, but often multiple name/type atoms don't fit on a line. Hmm - borderline?

In favor of using trailing commas beyond enum/list literals:

  • simplify manual edits
  • simplify implementation for code-generators, esp. macros
  • reduce false-positives in git diffs and hence work in code review
  • Emerging availability in other languages reflecting developer expectations
    • Perl, Ruby, JavaScript, CoffeeScript?
    • Not C, cpp, java, python?

Against:

  • Comma signifies continuation. No spoken language uses it as an ending delimiter
  • Trailing comma inline with trailing delimiter reads poorly and makes zero sense
  • Since optional, it becomes a point of formatting convention battles and policy
  • Support in other languages is (like Swift today) limited to array/list/enum literals
    • Liberal support is mostly restricted to less-safe languages
  • Generalizing instead of considering each case
    • conditions: if-let, guard
    • method/function parameter declaration/call
    • Type lists
    • Tuples
    • Parameter packs
    • enum cases
    • ...

For reference:
https://forums.swift.org/t/trailing-commas-in-all-expression-lists/19527

4 Likes

As a question, I presume "single element tuple/tuple type trailing comma", e.g: ("x",) and (String,) will not be permitted semantically. Though perhaps they should be valid syntactically?

2 Likes