Live data from Hacker News

Union types in C# 15

devblogs.microsoft.com

191–200 of 215 posts

Re: Union types in C# 15

#191

Earlier quoted context omitted.

It is a library call, but one that is tied to the behavior of a language feature (async/await). The reason I bring it up is that it is another one of those things where it matters in some cases depending on what you're doing. Look at the depths that Toub had to go through to explain when to use it: https://devblogs.microsoft.com/dotnet/configureawait-faq/ David Fowl concludes in the comments: > That’s correct, most o…

> It is a library call, but one that is tied to the behavior of a language feature (async/await). This is a good example of C# light-touch on language design. Async/await creates a state machine out of your methods but that's all it does. The language itself delegates entirely to platform/framework for the implementation. You can swap in your own implementation (just as it possible with this union feature) > So I bri…

> This is a good example of C# light-touch on language design.

Is it? F# code doesn't even need ConfigureAwait(false), one simply uses backgroundTask{} instead of task{} to ignore SynchronizationContext.Current, and this didn't require any language design changes at all (both are computation expressions), but it would for C# precisely because it delegates this choice to the framework.

Re: Union types in C# 15

#192
post #106

Hmm, they seem to have chosen to avoid names to the choices in the union, joining C++ variants and (sort of) TypeScript unions: unions are effectively just defined by a collection of types. Other languages have unions with named choices, where each name selects a type and the types are not necessarily all different. Rust, Haskell, Lean4, and even plain C unions are in this category (although plain C unions are not di…

Hi there! One of the C# language designers here, working on unions. We're interesting in both forms! We decided to go with this first as felt there was the most value here, and we could build the named form on top of this. In no way are we thinking the feature is done in C#15. But it's part of our ongoing evolution. If you're interested, i can point you to specs i'm writing that address the area you care about :)

Hi! Love to see that C# language designers are here on HN :)

Just wanted to add just another opinion on a few things.

I think many people already mentioned it, but I also don't feel to good about non-boxed unions not being the default. I'd personally like the path of least resistance to lead to not boxing. Having to opt-in like the current preview shows it looks like a PITA that I'd quickly become tired of.

Also, ad-hoc union types could be really nice. At least in Python those are really nice, stuff like `def foo(x: str | int)` is just very nice. If I had to first give this union type a name I'd enjoy it way less.

But I'm aware that you are trying your best to find a good trade-off and I'm sure I don't know all the implications of the things I wish you'd do. But I just wanted to mention those to have another data point you can weigh into your decision process.

Re: Union types in C# 15

#193
post #167

Earlier quoted context omitted.

In Rust's case, union types should only be used for FFI with a C ABI. As for C, it is a sharp blade on its own.

I do not agree. MaybeUninit is without any doubt more valuable than the C FFI use I can't even think of any prominent C FFI problems where I'd reach for the union's C representation. Too many languages can't handle that so it seems less useful at an FFI edge.

OS APIs for one, at least there are some Win32 calls that take unions if I remember correctly.

One of the reasons .NET had Managed C++, replaced by C++/CLI (nowadays C++20 compliant, minus modules), is exactly that P/Invoke (and RCW/CCW) cannot represent everything.

Which they don't want to expose on .NET type system directly.

Re: Union types in C# 15

#194
Interesting direction. It feels like languages are slowly moving towards more expressive and flexible type systems.

Even outside of C#, this trend seems to show up everywhere — trying to reduce boilerplate while keeping things safe.

Re: Union types in C# 15

#196

Earlier quoted context omitted.

Hi there! One of the C# language designers here, working on unions. We're interesting in both forms! We decided to go with this first as felt there was the most value here, and we could build the named form on top of this. In no way are we thinking the feature is done in C#15. But it's part of our ongoing evolution. If you're interested, i can point you to specs i'm writing that address the area you care about :)

Hi! Love to see that C# language designers are here on HN :) Just wanted to add just another opinion on a few things. I think many people already mentioned it, but I also don't feel to good about non-boxed unions not being the default. I'd personally like the path of least resistance to lead to not boxing. Having to opt-in like the current preview shows it looks like a PITA that I'd quickly become tired of. Also, ad-…

> Having to opt-in like the current preview shows it looks like a PITA that I'd quickly become tired of.

My belief is that we will have a `union struct` just like we have `record` and `record struct`. Where you can simply say you want value-type, non-boxing, behavior by adding the `struct` keyword. This feels very nice to me, and in line with how we've treated other similar types. You'll only have to state this on the decl point, so for each union type it's one and done.

Re: Union types in C# 15

#197

Earlier quoted context omitted.

FYI small string optimizations are generally implemented using unions.

In Rust? The two I'm big fans of, CompactString and ColdString do not use unions although historically CompactString did so and it still has a dependency on smallvec's union feature ColdString is easier to explain, the whole trick here is the "Maybe this isn't a pointer?" trick, ColdString might be a single raw pointer onto your heap with the rest of the data structure at the far end of the pointer, this case is expe…

I realise that I don't do the best job of explaining ColdString here. After all most 8 byte strings of UTF-8 text could equally be a pointer so, why can this work?

All ColdStrings which look like 8 bytes of UTF-8 text really are 8 bytes of UTF-8 text, just the type label on those 8 bytes isn't "[u8; 8]" an array of 8 bytes but instead "mut *u8" a raw pointer. "Validate" for example is 8 bytes of ASCII, thus UTF-8, and Rust is OK with us just saying we want a pointer on a 64-bit machine with those bytes. It's not a valid pointer, but it is a pointer and Rust is OK with that, we just need to be careful never to [unsafely] dereference the pointer because it's invalid

OK, so there are two cases left: First, what if there are fewer bytes of text? Zero even?

Since there are fewer than 8 bytes of text we can use the whole first byte to signal how many of the remainder are text, we use the UTF-8 over-long prefix indicator in which the top five bits of the byte are all set, bytes 0xF8 through 0xFF for this, there are eight of these bytes corresponding to our 8 lengths 0 through 7 inclusive. Because it's over-long this indicator isn't itself a valid UTF-8 prefix. Again we can pretend this is a pointer while knowing it's invalid.

Lastly, the seemingly trickiest problem, what if the string didn't fit inline? We use a heap allocation to store the text prefixed by a variable size integer length and we insist this allocation is aligned to 4 bytes. This means a valid pointer to our allocation has zeroes for the bottom two bits, then we rotate that pointer so those bottom two bits are at the top of the first byte position (depending on machine word layout) and we set the top bit. This is now always invalid UTF-8 because it has the continuation marker - the top bit is set but the next is not, which cannot happen in the first byte of any UTF-8 text, and so our code can detect this and reverse the transformation to get back a valid pointer using the strict provenance APIs if this marker is present.

This type is tomtomwombat's idea, credit to them:

https://github.com/tomtomwombat/cold-string*

Re: Union types in C# 15

#198
post #178

Earlier quoted context omitted.

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 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…

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 :)

Re: Union types in C# 15

#199

Earlier quoted context omitted.

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 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…

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 sy…

> Would something like `List = 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.

Re: Union types in C# 15

#200
post #178

Earlier quoted context omitted.

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…

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.

Post reply on HN