Trailing commas in all expression lists

+1 Yes, please

I wouldn't even mind these parsing ok, if it'll make the compiler's code simpler/better.

I would like to have a unifying theory as part of the proposal to explain the rationale for allowing trailing commas in some places but not other. I don't think it is a goal to allow people to add trailing commas to any syntactic list that includes commas. The utility argument here is based around the goal of allowing more free evolution and iteration of code.

For example, I could see a consistent argument along the lines of:

"swift supports trailing commas in comma separated lists that are variable length, or where elements are optional"

Such a policy would admit commas for array and dictionary literals, function calls with varargs or defaulted arguments. You could argue one way or other other to allow trailing commas in non-varargs calls that don't have defaulted arguments, but the specific description above would not allow it.

Similarly, I don't see a rationale to allow trailing commas in generic type lists. You can't just drop a type from the list, so the top level argument doesn't matter.

16 Likes

Assuming it doesn't break parsing, my personal preference would be for allowing no commas

I'd still be fine with this though

2 Likes

True, but in my opinion the main benefit of this is not to be able to add or remove easily, but to switch places as well.

big +1 for me as well! For all the reasons given above.

1 Like

I tend to agree with @Chris_Lattner3 on this (as I did in the original proposal). The rationale for the decision on SE-0084 was based around the idea that there were two kinds of comma-delimited lists to consider:

  • Homogeneous sequences where adding, removing, or reordering elements does not change the type or structure of the entity (array literals, dictionary literals, etc.)
  • Heterogeneous structured sequences where adding, removing, or reordering elements would change its structure or require other parts of the code to change at the same time (e.g., function calls and their call sites)

In that sense, the presence of a trailing comma is also a visual indicator that "this list can be extended without changing its semantic meaning." That provides more information to the reader than just "this is any comma-delimited list".

So Chris's suggestion about varargs function calls and optional arguments makes sense, and I could support that change as a way of making our existing behavior more consistent. Likewise, once we have variadic generics, I could see it being allowed there for the same reason.

However, allowing trailing commas everywhere feels like a solution that tries to change the language to get around the deficiencies of tools, and I'm not sure that's the right motivation.

Considering the SwiftSyntax/code generation case, since that's an area I've also worked heavily in and experienced the same pain points, the claim that it's somewhat annoying to work with comma-delimited lists when building/modifying nodes is definitely true, but to me, that doesn't say we should change the language to make that easier—we should improve the tooling by providing additional APIs to manipulate comma-delimited lists in a consistent way.

(IMO the SwiftSyntax API surface is far too low-level to be used directly as a code generator and it would be an improvement to provide a layer on top of it that lets users describe code entities in a manner that's not so close to the bare syntax, but that's a discussion for a separate thread.)

However, if there is compelling new information that should lead us to reconsider this after its initial rejection, it should be presented up front and explicitly in the new pitch so that we can consider it fully.

9 Likes

There are a few problems with this reasoning, and feedback since we accepted SE-84 indicates that these problems are real in practice. Code isn't static but changes over time, sometimes rapidly, so while a function or tuple may have a fixed set of arguments at a particular revision, the programmer may still be actively adding and removing parameters as they develop the code. I also don't think programmers are going to think in terms of "is this a variable length list", they're going to be thinking in terms of the syntax, and it's going to feel arbitrary (and already does, today!) that some lists are allowed to have trailing commas and some are not. "All trailing commas [in an expression] allow trailing commas" is also a consistent rule, it's a simpler rule, and it's a better rule that reflects the reality of how programs evolve.

12 Likes

I didn't state my preference for this, thinking no one else would want it. I don't know how "Swifty" it would be, but it sounds good at first glance.

My profession is COBOL developer, so I have a bias (at times; at others its horrid) toward some of its syntax. One of these is that commas are simply treated as whitespace. Generally, COBOL code contains no commas at all. All of the following COBOL statements are treated the same:

call sub1 using parm1 parm2 parm3
call sub1 using parm1, parm2, parm3,
call sub1 using parm1 
                parm2 
                parm3
call sub1 using parm1,
                parm2,
                parm3

I generally use the third option. Takes up more lines, but makes it easy to see the parameters. Of course in COBOL most code is not function calls, which is not the case for (all?) other languages.

Especially when considering that most (??) Swift methods have keyword parameters it seems like eliminating commas doesn't make it any more difficult to read. Take the following example:

let CVV = try! crypto.cvsGenerate(primaryAccountNumber: "44441111111119" 
                                  expirationDate: "0221" 
                                  serviceCode: "000"
                                  keyAIdentifier: "CVAKEY.TEST.00000001" 
                                  keyBIdentifier: "CVBKEY.TEST.00000001" 
                                  resultLength: 3)

While we're at it, why not make parentheses optional for "function calls" as well!

let CVV = try! crypto.cvsGenerate primaryAccountNumber: "44441111111119" 
                                  expirationDate: "0221" 
                                  serviceCode: "000"
                                  keyAIdentifier: "CVAKEY.TEST.00000001" 
                                  keyBIdentifier: "CVBKEY.TEST.00000001" 
                                  resultLength: 3 

Haskell is also a language that doesn't use commas or parentheses for function calls, except when required such as when one parameter is another function call, i.e.

let a = fun1 parm1 parm2 parm3 (fun2 parm1 parm2) parm5

Given that Swift is still, in the end, highly based on C I am guessing it would be a huge amount of work to do this, but I had to throw it out there. Take it for what its worth!

1 Like

Having worked on the parser in the deep past, I always thought the better solution was not trailing commas, but comma elision if all of the elements are on separate lines. I thought this would solve most of what people wanted most of the time, and with less syntax. For example:

let x = [
    "hello"
    "world"
] // or ["hello", "world"]

In other words, either use commas between all of the elements, or newlines. Pick one.

2 Likes

Why is the trailing newline allowed (or is it required?) but the trailing comma isn't?

2 Likes

It should definitely be allowed. this is the layout I use all the time (with commas). I'm not sure about this syntax though. It would need to work with nested collections, multi-line method calls, etc. If we can't make it work in all contexts including arbitrarily nested comma-separated lists then it would be another set of special-case rules to remember. If we can make it work without risk of ambiguity or other confusing user-facing errors then it would be nice. (although it would take some re-wiring of my brain to get used to :slight_smile:)

1 Like

Hi @scanon,

Newlines are for the most part, "just" whitespace in Swift. An exception is made when people try to cram things onto one line, and then a delimiter is forced upon the user to catch typos. For example, semicolons.

Said differently, when I last worked on the parser (long ago), the grammar was tight enough that the comma delimiters weren't needed to disambiguate anything other than typos. Things may have changed since then, but I hope not by much.

1 Like

One approach that has come up a few times is to allow the commas to be omitted in favor of
newlines. Unfortunately, that approach allows for ambiguities:

struct Gadget {
    static var bar: Gadget = Gadget()
    var bar: Gadget { return self }
}

func foo() -> Gadget {
    return Gadget()
}

let gadgets: [Gadget] = [
    foo()
    .bar
]

In the above, should gadgets be an array consisting of two gadgets foo() and Gadget.bar or just one foo().bar?

The ambiguity could perhaps be resolved by imposing still more whitespace requirements such as requiring instance members be indented if they appear on the line after the instance they are called on

[
    foo()
        .bar  // an instance member because of the indentation
]

but there doesn't seem to precedent in the grammar for such a requirement.

The proposal has been updated with using newlines as a separator in the alternatives considered.

5 Likes

Yeah, allowing newlines is an interesting thing to explore too, but I think consistently allowing trailing commas would be good progress that doesn't prevent us from more thoroughly exploring newlines without commas separately.

12 Likes

I think you are arguing that fixed function calls should allow comma elision, and I'm not opposed to it if we think this is essential behavior for other function call cases. The most compelling argument to me personally is your: """I also don't think programmers are going to think in terms of "is this a variable length list", they're going to be thinking in terms of the syntax""" point.

That said, I don't see how this applies to type lists.

-Chris

Well, I seem to be quite alone with my opinion here, but I personally find trailing commas just awful.
For me a trailing comma signifies that the programmer has forgotten to type something, i.e. an error.

I would rather prefer an improvement of diffing tools. My trusty old Smalltalk IDE from more than 10 years back did code diffing on the syntax tree level, i.e. independent of formatting. Miles ahead of the line based text diffing which is ubiquitous today.

I think it is sad when a language is changed for the worse just because of bad tools...

5 Likes

For list contents, not allowing trailing commas hurts me, as the developer. It makes editing the list that much more annoying, and why should that be the case when the compiler is perfectly capable of ignoring the presence of unnecessary punctuation? It doesn't make anything less clear.

I do agree that a trailing comma in a function call site would be weird and look like something was missing.

2 Likes

+1 for allowing trailing commas

My two cents:

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

… does not solve the problem, it just moves it to the first line.

That said: If trailing commas are okay, shouldn't leading commas be okay, too?

What about double commas:

print(
    "red",
  // "green"
  , "blue"
)

So, actually, I’m +1 for dangling commas.

1 Like

I find the motivating example fairly unconvincing, because I've never seen anyone write function calls like that. This is essentially the same reasoning that led to SE-0084 being rejected. I don't think this change would be harmful (aside from being very ugly) but I'm not seeing much upside either.