dotnet / csharplang

The official repo for the design of the C# programming language
11.45k stars 1.02k forks source link

Proposal: Permit trailing commas in method signatures and invocations #391

Open MgSam opened 7 years ago

MgSam commented 7 years ago

Migrated from https://github.com/dotnet/roslyn/issues/12496

Proposal

Allow trailing commas in method/constructor declarations and invocations.

void Foo(
    int bar, 
    int baz,
) { }

Foo(
    bar: 5,
    baz: 3,
);

Rational

One very small but appreciated feature in C# is the tolerance of the compiler for trailing commas in array initialization or object initialization:

var a = new int[] 
{ 
    3, 
    4, 
    5, 
};
var b = new 
{ 
    Bark = "woof", 
    Bite = "chomp", 
}

This makes it easier to reorder or edit the item order without taking time to make sure that last comma gets deleted.

It would be nice if this feature also extended to method signatures and invocation. It would be especially useful in the post C# 4.0 where named parameters can be reordered or used to make a highly descriptive method call.

Example

void Foo (int bar, String baz, Func<int> cat, Action<String> dot, int elf) { }

Foo(
    bar: 5, 
    baz: "",
    cat: () => 6,
);

Wait, having cat last is confusing, let me place it after bar...

//Rather than just copy and paste the relevant line...
Foo(
    bar: 5, 
    cat: () => 6, //Now I need to add this comma here!
    baz: "" //And I need to remove this comma here!
);

Instead, if method invocations were also tolerant of an extra comma, it would make editing easier:

Foo(
    bar: 5, 
    cat: () => 6, 
    baz: "", //Extra comma here, it's not hurting anyone!
);

Final thoughts

TypeScript has and soon JavaScript will have this feature. I think it will work great in C# as well.

DavidArno commented 6 years ago

@zippec,

If I saw:

if (condition)
    action1();
    action2();

then I'd see a very likely bug: the author meant for action2() to only run when condition is true, which isn't the case. But your example is just "meh" to me; there is no confusion and it certainly isn't equivalent to the hideousness of trailing commas.

Plus "the language is already full of mistakes; what harm some more?" is a pretty weak argument.

JeroMiya commented 5 years ago

I don't really experience the issue with diffs, but I definitely noticed this issue when attempting to refactor some code to use read-only data structures instead of get/set properties. I would previously use the object initializer:

var foo = new Foo() {
  Property1 = "value1",
  Property2 = "Value2",
};

And this was nice, especially early in the development of a feature as I add or remove or re-order properties from the list, I don't have to waste time and attention maintaining the comma on the "last property" in the list here. It's just part of my muscle memory to add a comma to every property in the list. That was the whole point of C# being permissive with that last comma in object and array and dictionary initializers. It's a great feature of C# that is a good quality of life improvement.

But now, when I refactor the above code to use read-only data structures, say for a uni-directional data flow pattern (so I can enforce read-only state at the compiler level, instead of merely relying on convention), then I lose that "last line comma" permissiveness, and just from personal experience I can tell you I REALLY notice the difference and it's extremely annoying:

// aside: C# *really* needs a better syntax for read-only records like this, like F# has.
// read-only data structures in C# require *painful* amounts of boilerplate.
// so much so that I often seriously consider defining them all in an F# class library.
class Foo() {
  public Foo(string property1 = null, string property2 = null, string property3 = null)
  {
    Property1 = property1;
    Property2 = property2;
    Property3 = property3;
  }
  public string Property1 { get; }
  public string Property2 { get; }
  public string Property3 { get; }
}
var foo = new Foo(
  property1: "value1",
  property2: "value2"
);

To be honest, having to maintain that last line in a comma separated list of function arguments is one of my pet peeves, just as it is with object and array initializers, and is just one of several syntactic speed bumps for creating read-only data structures in C#.

Oh, and this:

var foo = new Foo(
    property1: "value1"
  , property2: "value2"
);

Is not a tangible improvement. You've just moved the comma maintenance to the first line instead of the last line.

Is this is a high priority feature? No. Would it make me happier? Yes. Would it make me measurably more productive? Marginally, yes.

Does it result in ambiguities? Not if you ignore the last comma in an argument list, when no value is provided:

// semantically, these two statements are equivalent, and should result in the same AST:
Foo(1, 2);
Foo(1, 2,);

I am also OK with people using them in argument lists on the same line, especially if there are more than three arguments:

// Now I can add/remove/reorder this argument list with a simple copy/paste
FunctionWithLotsOfArguments(arg1: 1, arg2: 2, arg3: 3, arg4: 4,); 
MgSam commented 4 years ago

Kotlin just added this feature as well.

https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma

CyrusNajmabadi commented 4 years ago

I'm championing this.

Pzixel commented 4 years ago

It's funny to see that 3 years later after making an acquaintance with several languages including Rust, Idris and some others I've changed my position in 180 degrees and now I think it's pretty valuable, for better git diffs at least πŸ˜ƒ

alrz commented 4 years ago

@CyrusNajmabadi Would this be only about method arguments/parameters or all the lists? e.g. tuples, type parameter constraints, and so on. (for an existing instance I think Rust does permit all those)

YairHalberstadt commented 4 years ago

This is particularly useful for source generators, where having to deal with the lack of a trailing comma tends to add a few extra lines of code to any loop printing parameter or argument lists. Whilst on each case its simple, overall it adds up to a lot of clutter in the source generator.

datvm commented 3 years ago

I want to add a use case for this suggestion, for the new record syntax, this would be awesome (currently impossible):

    public record Foo (
        int bar1,
        string? bar2,
    );
mikernet commented 3 years ago

There is another case where I think a trailing comma should be allowed that I don't believe was mentioned: collection initializer Add method parameters, especially when they are params. For example:

public class Simple : IEnumerable<(string, string, string)>
{
    public void Add(string x, string y, string z) { }
}

var a = new Simple() {
    {
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf",
        "aswefa4rguikareg", // this comma is not allowed
    },
};

public class WithParams : IEnumerable<IReadOnlyList<string>>
{
    public void Add(params string[] x) { }
}

var b = new WithParams () {
    {
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf",
        "aswefa4rguikareg", // this comma is not allowed
    },
    {
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf", // this comma is not allowed
    },
    {
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf",
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf",
        "fkeuahwkeufhawefe",
        "afkuehafkuehkuehf",
        "aswefa4rguikareg", // this comma is not allowed
    },
};
Pzixel commented 3 years ago

I think trailing comma should be available in any context that accepts array of arguments. It could be just an optional trivia for any SeparatedList

MixusMinimax commented 1 year ago

I strongly agree with this. For the people that find that an extra comma hurts readability, how are you able to read the arguments that come before? Those have a comma after them, and you don't complain there.

Merge conflicts are a huge issue. Sure, there are syntax-aware diff systems, which I hope become more prevalent in the future, but for now, diffs are line based. Adding a new field to a record should not modify two lines.

theunrepentantgeek commented 1 year ago

For the people that find that an extra comma hurts readability, how are you able to read the arguments that come before? Those have a comma after them, and you don't complain there.

My objection comes from the cognitive science around how people read.

For people whose first reading language uses a comma as a phrase separator (*), the presence of a , is a subconscious visual indicator that there's more to come in this sentence.

(*) From what I've read, it's less clear whether this applies to people whose first language has different forms.

When you read foo, bar, baz each comma is understood by your brain as indicating there's more to come.

When you read foo, bar, baz, your brain has to do a doubletake because the promise made by the , that there's more to come is immediately violated.

Redundant trailing commas are a cognitive speedbump. There's very strong evidence from the natural language field that shows incorrect punctuation greatly slows down comprehension.

My personal experience working in Go backs this up. It's like having an uneven brick in your garden path that you trip on most every time you check the letterbox.

Merge conflicts are a huge issue. Sure, there are syntax-aware diff systems, which I hope become more prevalent in the future, but for now, diffs are line based.

And this is exactly why we should not be changing the language.

Language changes are forever; they can never be undone.

Why should we make a permanent change to the language just to accommodate tooling that hasn't yet caught up?

Language changes are supremely expensive, both to make and to maintain. It would be cheaper to fix the tooling.

jnm2 commented 1 year ago

My experience is that ite roadbump disappears quickly with use. I switched over for multiline initializers, where the comma appears consistently at the end of the lines which end each item.

Socolin commented 1 year ago

For me it would remove the "noise" of all the removed/added lines due to the addtion of a coma, during code review.

Example:

public SomeClass(
    IDeps1 deps1,
-   IDeps2 deps2
+   IDeps2 deps2,
+   IDeps3 deps3,
)

I would need this only for multi line initializer, it does not make sence in single line. And like ; at the end of each line, I think the brain can understand that , at the end of each line of a parameters is a part of the syntax.

public SomeClass(
    IDeps1 deps1,
    IDeps2 deps2,
    IDeps3 deps3,
)

@theunrepentantgeek I'm not sure to understand what the point with one-liner example, since this feature really seems to be useful in a multiline example (I don't see the point in adding extra , if the code is on one line), and I may be wrong but when I'm seeing this, I see this as a list and not as a sentense for which I' m looking for "what next"

foo,
bar,
baz, 

Language changes are supremely expensive, both to make and to maintain. It would be cheaper to fix the tooling.

Changing all the diff tool and merge tool and git blame, to undestand that specific case to improve the diff does not seems cheaper. And I don' t think git will start parsing C# and understant that a coma added between 2 parameters should be ignored anytime soon.

Logerfo commented 1 year ago

Reducing noise in diffs is the main reason I always use trailing commas in multi-line scenarios. Allowing them in parameter and argument lists would cover this noticeable feature gap.

JeroMiya commented 1 year ago

I don't think I would want a diff tool to hide a changed line, even if it were capable of it through syntax aware diffing.

theunrepentantgeek commented 1 year ago

I'm not sure to understand what the point with one-liner example

I was using a single line because I was giving a natural language example, not a code one.

That said, C# has never been a whitespace sensitive language, so it's irrelevant whether we're talking single- or multi-line. If a trailing comma is permitted in one place, it will be permitted in the other.

Changing all the diff tool and merge tool and git blame, to undestand that specific case to improve the diff does not seems cheaper.

Language changes are extremely expensive, which is why they get discussed and debated endlessly before they're made.

My guestimate is that the cost of changing the language would exceed the cost of updating the tools by at least an order of magnitude (that is, 10x).

My experience is that ite roadbump disappears quickly with use.

We developers can get used to most anything. We're smart like that.

In this case, however, given the hestitation that is built into the way our brains parse language (at an autonomic level), it's not something we can ever avoid, but instead just something we get used to.

I suspect this kind of adaptation is like learning to avoid the squeaky step on a staircase: you stop irritating people in the middle of the night, but the problem is still there.

I've gotten used to trailing commas in Go, simply because I have no choice. But that doesn't mean I like them - because I'm aware of the science that makes them a foolish quirk of the language.

Socolin commented 1 year ago

While speaking about tooling, if the only reason not to add the coma is a preference and readability issue, I think then we should allow this and add a setting in the IDEs to show/hide the trailing comma.

This would allow to fix problem with the diff tools and not add new problem for humans.

The comma could be added automatically and hidden depending on the settings of the project / IDE

miloush commented 1 year ago

I wouldn't support IDE hiding parts of content of the file, but aren't all these cognitive arguments resolved by just not using the feature if it makes the experience worse for you?

snebjorn commented 1 year ago

JavaScript was able to do trailing commas and it's awesome! JavaScript pretty must runs the world. so if that was able to do it then C# also should πŸ˜„

I see comments about "preference and readability". Lets not forget that the trailing commas are optional. If you don't like them or it makes it harder for you to read. Then don't use them.

MixusMinimax commented 1 year ago

Cognitive science is mainly to be applied to natural languages. A list of arguments is not a sentence, it is a list of arguments. And yes, if you don't like it, then don't write trailing commas, that's what formatters are there for to enforce. But the arguments for merge conflicts, consistency, etc. strongly outweigh "idk it's not as nice to read". That's an argument to not use the feature, sure.

jnm2 commented 1 year ago

That said, C# has never been a whitespace sensitive language, so it's irrelevant whether we're talking single- or multi-line.

For questions of formatting and readability, whitespace has never been irrelevant.

theunrepentantgeek commented 1 year ago

Lets not forget that the trailing commas are optional. If you don't like them or it makes it harder for you to read. Then don't use them

This is only relevant for toy projects that will only ever have a single developer.

As soon as you start working on anything with a meaningful size, you're looking at contributions from multiple developers over multiple years.

Later devs don't get a choice, they have to deal with whatever their predecessors have done.

Arguments of "If you don't like it, don't use it" always founder on this fact.

snebjorn commented 1 year ago

As soon as you start working on anything with a meaningful size, you're looking at contributions from multiple developers over multiple years.

Later devs don't get a choice, they have to deal with whatever their predecessors have done.

Code style is a team decision. They can change in the lifetime of a project. When they do you run the formatting tool with the new settings. I don't see a problem here.

mrwensveen commented 1 year ago

For me, the inconsistency between initializing a record type (constructor) and a class with properties annoys me the most because the use case is so similar. If it only were to be allowed in collection initializers then I would be okay with it (kind of).

Some people have argued that it's bad enough that C# allows it in other places but that's a matter of opinion. The feature clearly made it through, presumably with people arguing for and against the feature.

So, while I think this feature would be nice, I feel more strongly about the language being inconsistent with regards to trailing comma's.

bw-flagship commented 1 year ago

The (quite new) language "dart" allows trailing commas. IDEs make use of this and automatically break lines if you add a trailing comma on method calls, constructors, etc. Switching from back to coding c# from coding dart feels like a downgrade because this is not possible.

KennethHoff commented 11 months ago

With the introduction of Primary constructors I think this "issue" should be looked at. You may wonder what Primary Constructors have to do with anything, so here's my argument (pun intended):

Previously whenever you wanted to add or remove a field from a class you had to do it many places; Add the field, add a parameter to the constructor and set the field from the parameter (And obviously the same thing in reverse for removing), so adding/removing a single comma wasn't that big of a deal.

Now on the other hand you only have to change a single line to do all those three things mentioned previously, so the removal of a comma is now a majority of the time spent. Instead of simply pressing shift-delete (default keybind in Rider for deleting an entire line), you also have to go up a line and remove the comma.

(This is in addition to all the other arguments mentioned above, like consistency with initializers)

beppemarazzi commented 11 months ago

Or, in other words, in many context (i.e. initialization list, argument list, ...) the comma will act as a "terminator" not as a "separator". Exactly like the semicolon does with statements. (https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(syntax)#:~:text=A%20statement%20separator%20demarcates%20the,end%20of%20an%20individual%20statement.). It's another (little) point in favor of this proposal to make the whole language more self consistent...

Bigsby commented 8 months ago

This would improve code versioning because, when adding items to lists (e.g., function parameters) only the added line is marked as changed and not the previous last line where the comma was added.

Foxtrek64 commented 4 months ago

Coming over from #8190

This is very useful when targeting multiple TFMs. Take this example that's currently impossible today:

/// <summary>
/// Represents a 9-digit United States social security number.
/// </summary>
[PublicAPI]
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public readonly struct SocialSecurityNumber
    : IFormattable,
    IEquatable<SocialSecurityNumber>,
#if NET7_0_OR_GREATER
    IEqualityOperators<SocialSecurityNumber, SocialSecurityNumber, bool>,
#endif
#if NET8_0_OR_GREATER
    IUtf8SpanFormattable,
    IUtf8SpanParsable<SocialSecurityNumber>
#endif
{
    // Specification omitted
}

Such is only possible with trailing commas allowed.

There are a couple work-arounds, such as SQL-style commas, but style rules may prohibit this:

/// <summary>
/// Represents a 9-digit United States social security number.
/// </summary>
[PublicAPI]
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public readonly struct SocialSecurityNumber
#pragma warning disable SA1001 // Commas should not be preceeded by whitespace.
    : IFormattable
    , IEquatable<SocialSecurityNumber>
#if NET7_0_OR_GREATER
    , IEqualityOperators<SocialSecurityNumber, SocialSecurityNumber, bool>
#endif
#if NET8_0_OR_GREATER
    , IUtf8SpanFormattable
    , IUtf8SpanParsable<SocialSecurityNumber>
#endif

Partial classes are another option, but this doesn't make sense in all cases.

mikernet commented 4 months ago

@Foxtrek64 Just FYI, you can also do:

public readonly struct SocialSecurityNumber :
#if NET7_0_OR_GREATER
    IEqualityOperators<SocialSecurityNumber, SocialSecurityNumber, bool>,
#endif
#if NET8_0_OR_GREATER
    IUtf8SpanFormattable,
    IUtf8SpanParsable<SocialSecurityNumber>,
#endif
    IFormattable,
    IEquatable<SocialSecurityNumber>
{
}
aradalvand commented 3 months ago

The use cases are patently clear and justified. Source generators, TFMs, consistency, diffing, etc. This one little trivial-to-implement feature, which almost every other modern language already has, would improve the experience in all those areas.

So why not just do it and get it over with?

CyrusNajmabadi commented 3 months ago

So why not just do it and get it over with?

Because our plates are full with lots of other work.

miyu commented 1 month ago

If this is implemented, it'd be consistent to support trailing commas in array indexers too. This would be useful for a variety of niche DSLs that I've seen... Trivial examples would be DSLs which index by filter condition or slice N-dimensional data grids.