Hacker News new | ask | show | jobs
by Metasyntactic 64 days ago
Hi there. Designer of this feature :D

> is non-intuitive and unnecessary.

intuitive is definitely in the eye of the beholder. When people saw:

`HashSet<string> people = [with(StringComparer.CaseInsensitiveComparer), .. group1, group2]`

they found it understandable. And this was also much nicer than what they'd have to write today (which would bring them out of the nice declarative collection-expression space).

Does that make it 'necessary'? Ultimately that's up to the individual. We felt like it was. Not being able to do simple things like this felt like a 'bitter pill'. Customization of collection construction is common (looking in codebases, it shows up about 7% of the time). So having to 'fall out' from the uniform collection-expr system into the much more verbose and clunky forms just for this common enough case felt 'necessary' to us.

>But I feel that there has to be a direction, things have to work together to make a language feel coherent.

I feel like this is conflicting feedback. Collection expressions made the language more coherent. Instead of 7 different ways of doing things (some of which were genuinely not efficient), we gave one uniform way of doing it. That makes things more coherent. Making it so you don't have to drop out of that for something as simple as configuring the collection makes things more coherent.

4 comments

As someone who has been coding C# since the pre-generics days, this is the first syntax change which I strongly disagree with. I pretty much love every little bit of syntactic sugar you guys have added to the language. But this? This seems objectively illogical and just straight up ugly. It blows my mind that this is making it into the language, and it makes me worry about the future of C#.
How do you define "objectively illogical" here?
Just guessing here: Putting something at index 0 that has nothing to do with the content.

I tend to agree, but I also didn’t try it.

I also think the 7% number is wrong. 0.5% seems more realistic. It’s just that you are not able to see the majority of the code. Not everything is checked in or on GitHub.

I equate this language addition to the same mistake that !! was going to be a few years ago, until it got all that discussion threads on Twitter.

C# doesn't need to have syntax sugar for every possible use case.

Some of the more recent features feel like the outcome of the team pressure to have new language features to announce in November every year.

There isn't any such pressure. These features only happen because someone goes out of their normal job space to push for the necessity for them. All of the design team have full time work on other things. The design and impl only happens if the whole team can be convinced that it is important and worth investing in. Note that a lot of that convincing goes from the tons of feedback we get everywhere. This is anywhere from github, to partners (first, second, third), to conferences, forums, hacker news etc. etc. etc. We have tons coming in constantly. We pick these items up and spend this time on it precisely because we've seen the problems, and how it is affecting the ecosystem, and our future goals there, and we think it is then worthwhile.

I understand you feel this is ilke `!!`. We do not. We think being able to amke a dictionary, and pass in a custom comparer is deeply important. Analyzing code out there, we find that this happens in anywhere from 5-10% of all dicts. That is a ton of codebases and users impacted, and we've already heard from many of them about the friction this causes. Simply discarding that group greatly undercuts one of the core value props that collection expressions brings. A uniform and simple syntax that should suffice for nearly all collection needs.

You may feel differently. That's life in the design world :)

Well, your employers are the opinion language sugar doesn't matter in the CoPilot agentic world, so there is that.

As such my opinion remains that in such context saving keystrokes isn't that high priority, when agents do the actual work.

Thanks for replying, though.

Practically speaking, I've found that Claude never uses collection expressions, so the feature has disappeared from my code. Before AI, the feature was looked at with skepticism by my coworkers. We like writing "var" for all variable declarations. You have to write the type on the left side if you want to declare a variable with a collection expression, and we would never do that otherwise. Can't do `foreach (var x in [1, 2, 3])`. Too often, you have to make specific accommodations in your code to allow the collection expression to be valid.

Collection expressions today are more the sort of thing that a code poet or golfer can do to prettify their code than something a newbie can count on using. It's tough to explain "you can only use this when the collection type is implied in that spot" to a newbie. The value of the base feature is still unproven for me. I'm not sure I agree, without some convincing, that collection expressions made the language more coherent rather than doing https://xkcd.com/927.

Hi, thanks for answering :)

> Collection expressions made the language more coherent. Instead of 7 different ways of doing things (some of which were genuinely not efficient), we gave one uniform way of doing it.

I see your point on this. My dislike comes from a mixture of "I don't like how it looks" and "this language already has tons of features".

In terms of looks, I wish it could be more coherent with existing syntax.

List<int> = new {1, 2, 3} and List<int> = {1, 2, 3} are obviously taken up by anonymous types and blocks themselves. Would something like

List<int> = new(capacity: 10)[1, 2, 3]

have been possible? It feels like a combination of target-typed new and the initialization syntax. It involves the "new" keyword, which everybody already associates with constructor calls. It's short. Obviously, I don't know if this even works, maybe there's a parsing issue there (aren't those the most annoying issues in language design haha).

> they found it understandable

Kind of in my experience. Me and the people I've shown this to can easily remember it, but we all agree that it doesn't look like obvious syntax to them. Those two things are quite different to me. Contrast this to something like target-typed new, which immediately made sense to the same people. One might argue that that's fine enough and maybe it is, but I think, the less I have to remember about a language's syntax, the better. I'm going to have to remember many many other things anyway, better keep my memory free for the details of SynchronizationContext and async flow :)

I'm obviously aware that you get tons of bikeshedding comments like this all the time, so I'm sure you've gone through this. But to me, this invented syntax would have been fine. I just don't like the one that actually got in.

Now, the necessity on the other hand: May just be the company I'm working at, but my personal experience has never been that this is a big issue. Sure, it's nice to not have to fall back to explicit initialization a few more times. But personally, this doesn't pass my threshold of "painful enough to warrant additional syntax".

That's the core of my issue: Most, maybe all, of the new features in the language are fine to me in isolation. I may bikeshed about the explicit syntax (see: this thread). But my main issue is that the sum of complexity in the language and the issues beginners have when learning it are steadily increasing. I see this all the time at work.

As you said, this is definitely subjective. And in the end, language design is a very subjective process and maybe C# just won't be for me in the long run. But I wish it would, because at its core I like it, and .NET, a lot. Which is why I will continue to speak for my (subjective) viewpoint.

Well, this turned into a bit of an incoherent rant. I appreciate you exposing yourself to the HN acid pit ;)

> Would something like `List<int> = new(capacity: 10)[1, 2, 3]` have been possible?

Great question. And our design docs, and discussion with the community cover this. The reason that was eliminated as an option (we considered several dozen possible syntaxes) was that this syntax was actively confusing and misleading for people (for several reasons). These include (in no particular order):

1. the use of 'new' indicating that a new value was being allocated. That's not necessarily the case with collection expressions. The compiler is free to be smart here and not allocate if it doesn't need to. `[1, 2, 3]` for example, being constants, can in some cases just point at a data segment in the program.

2. the use of 'new' indicating that a constructor is being called ('new' has always meant that). That's not necessarily the case with collection expressions. Many collection forms (interfaces, immutables, spans, etc) do not go through constructors. This was actively confusing for people.

3. That syntax is already legal. It's an implicit objet creation that is being indexed into.

4. There was strong feedback from many in the community (and the design group, and lots that we talked to) that having things outside the boundary of the `[ ... ]` syntax was actively confusing. One could not easily tell what hte collection was and what wasn't part of it. The idea is that the `[ ... ]` is "the actual value". You know where it starts, where it ends, and what it represents.

--

Of course, at the end of the day, absolutely none of this may sway you. That's why we have a design process and we go through so many options. There were dozens considered here and we had many axes we were trying to optimize for. Overal, this struck a balance of working nicely, and having no major problems going for it (unlike other options).

> I'm obviously aware that you get tons of bikeshedding comments like this all the time, so I'm sure you've gone through this.

Yup :) Totally ok with us though.

> But personally, this doesn't pass my threshold of "painful enough to warrant additional syntax".

Sure. But that's why we look at the entire ecosystem. And we converse with people who have full codebases they haven't been able to move over because of the lack of this. And we look at the pain that this will cause esp when we get dictionary/key/value support. All of this motivated what was ultimately a tiny feature that cost very little to get in. It was medium bang for very low buck.

And that's worth explaining too. We are always working on some huge features. But they take up a ton of time and need tons of effort and runway. Small features like this are easy to slot in in gaps and help deal with papercuts and friction that are often annoying people.