42 comments

[ 1.8 ms ] story [ 75.8 ms ] thread
I haven't read this in detail but I expect it to be the same kind of sealed type that many other languages have. It doesn't cover ad-hoc unions (on the fly from existing types) that are possible in F# (and not many non-FP languages with TypeScript being the most notable that does).
Third paragraph from the top:

> unions enable designs that traditional hierarchies can’t express, composing any combination of existing types into a single, compiler-verified contract.

> It doesn't cover ad-hoc unions

Yes and no. C# unions aren’t sealed types, that’s a separate feature. But they are strictly nominal - they must be formally declared:

    union Foo(Bar, Baz);
Which isn’t at all the same as saying:

    Bar | Baz
It is the same as the night and day difference between tuples and nominal records.
Hi there! One of the C# language designers here, working on unions.

We're very interesting in this space. And we're referring to it as, unsurprisingly, 'anonymous unions' (since the ones we're delivering in C#15 are 'nominal' ones).

An unfortunate aspect of lang design is that if you do something in one version, and not another, that people think you don't want the other (not saying you think that! but some do :)). That's definitely not the case. We just like to break things over many versions so we can get the time to see how people feel about things and where are limited resources can be spent best next. We have wanted to explore the entire space of unions for a long time. Nominal unions. Anonymous unions. Discriminated unions. It's all of interest to us :)

IME, this is a good thing.

The problem with ad-hoc unions is that without discipline, it invariably ends in a mess that is very, very hard to wrap your head around and often requires digging through several layers to understand the source types.

In TS codebases with heavy usage of utility types like `Pick`, `Omit`, or ad-hoc return types, it is often exceedingly difficult to know how to correctly work with a shape once you get closer to the boundary of the application (e.g. API or database interface since shapes must "materialize" at these layers). Where does this property come from? How do I get this value? I end up having to trace through several layers to understand how the shape I'm holding came to be because there's no discrete type to jump to.

This tends to lead to another behavior which is lack of documentation because there's no discrete type to attach documentation to; there's a "behavioral slop trigger" that happens with ad-hoc types, in my experience. The more it gets used, the more it gets abused, the harder it is to understand the intent of the data structures because much of the intent is now ad-hoc and lacking in forethought because (by its nature) it removes the requirement of forethought.

    "I am here.  I need this additional field or this additional type.  I'll just add it."
This creates a kind of "type spaghetti" that makes code reuse very difficult.

So even when I write TS and I have the option of using ad-hoc types and utility types, I almost always explicitly define the type. Same with types for props in React, Vue, etc; it is almost always better to just explicitly define the type, IME. You will thank yourself later; other devs will thank you.

Yeah, Typescript feels like it had has arrived at the point where someone needs to write “Typescript: the good parts” and explains all of the parts of the language you probably shouldn’t be using.
it's basically `union <name>([<type>],*)`, i.e.

=> named sum type implicitly tagged by it's variant types

but not "sealed", as in no artificial constraints like that the variant types need to be defined in the "same place" or "as variant type", they can be arbitrary nameable types

I'm pretty sure at one point there was proposal that allowed declaring something like `int or string`. Not sure what happened with it though.
It's very disappointing that they aren't supporting Rust-style discriminated unions.
Hi there! One of the C# language designers here, working on unions. We're extremely interested in discriminated unions. A real problem is that there so much interest, with many varying proposals on how best to do them. It's a lot to go through, and we've found some of the best designs layer on standard unions. So we like this ordering to lay the foundation for discriminated unions to built on top of! :)
These are discriminated unions, even if they may be not Rust-style.

You can see in the examples, how "switch" uses the implicit discriminant of the union to select the code branch that must be executed, depending on the current type of the value stored in an union.

The syntax of the "switch" seems acceptable, without too many superfluous elements. It could have been made slightly better, by not needing dummy variables that must be used for selecting structure members (or used in whichever else expression is put in the right hand side; the repetition of the dummy variable names could have been avoided if the name of the union variable could have been reused with a different meaning within a "switch", i.e. as a variable of the selected type).

I do not see what else do you want. Perhaps Rust has reused the traditional term "discriminated unions", which had been used for many decades before Rust, and which means the same thing as the more recent terms "tagged unions" or "sum types", with a non-standard meaning and you have that in mind.

ML-style discriminated unions, actually.
Is this the last of the F# features to be migrated into C#?

What a missed opportunity. I think really F# if you combine all of its features, and what it left out, was the way. Pulling them all into C# just makes C# seem like a big bag of stuff, with no direction.

F#'s features, and also what it did not included, gave it a style and 'terseness', that still can't really be done in C#.

I don't really get it. Was a functional approach really so 'difficult'? That it didn't continue to grow and takeover.

Purity is overrated. C# is a kitchen sink language but you need give credit to the language designers. Compared to C++, for example, C# feels feature rich and consistent even though it abandons purity.
That's a very low bar. Any language feels consistent compared to C++.
Is C# a great language trapped in a terrible ecosystem? ie would masses use C# if it existed in another ecosystem?

Or is it becoming a ball-of-mud/bad language compared to its contemporaries?

(Honest questions. I have never used .NET much. I'm curious)

C# can be used inside Unity game engine. Does this makes it trapped?
> terrible ecosystem

.NET is a fantastic ecosystem. Has a decent build and dependency system (NuGet, dotnet run/build, declarative builds in XML). Massive standard library, with a consistent and wide focus on correctness, ergonomics, and performance across the board.

You can write everything in many languages, all on the same runtime: business logic in C#; hot paths interfacing with native libraries in C++/CLI; shell wrappers in PowerShell, document attachments with VB, data pipelines in F#.

I feel more people should use it, or at least try it, but sadly it is saddled with the perception that it is Windows-only, which hasn't been true for a decade (also, IMO, not necessarily a negative, because Windows is a decent OS, sue me).

I work with C#/NuGet on windows every day and my experience is entirely the opposite.

The build and dependency systems are an abysmal esoteric, poorly documented mix between csproj files, sln files, random scattered json files, etc.

The standard library in my experience sucks and has all sorts of issues, especially around Uris, DateTimes, etc.

And the ecosystem itself has such a low quality bar, ironically _especially_ with anything made by microsoft. For every nuget package that's well-designed, well-documented, and easy-to-use, there's five which have bugs and undocumented exceptions and poorly-designed APIs.

Its a great language in a very good ecosystem. Try it. Its great.

It has a bad rep because Microsoft could Microsoft as they do.

Why is the ecosystem bad? I haven't ran any .net code on anything but Linux in years. The open source community is great. I don't know why it gets a bad rep.
So they finally took all of the cool features from F#. What's missing? The pipe operator for railway oriented programming?
This is HUGE! Now we can use mostly functional programming in C#. This feature was requested since many years ago.

The only thing I wish now is for someone to build a functional Web framework for C#.

I love it, but I see a downside, though: unions are currently implemented as structs that box value types into a Value property of type object. So there can be performance implications for hot paths.
I don't love OneOrMore<T>

It's trying to generalize - we might have exactly one T, fine, or a collection of T, and that's more T... except no, the collection might be zero of them, not at least one and so our type is really "OneOrMoreOrNone" and wow, that's just maybe some T.

I have the same gripe about IsNullOrWhiteSpace which is basically an extension to IsNullOrEmpty, so it really should be named IsNullOrEmptyOrWhiteSpace.
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 discriminated at all, so they’re not nearly as convenient).

I personally much prefer the latter design.

I think Zig does a very good illustration of this distinction. They separate:

- enum: essentially just a typed uint tag collection

- union: the plain data structure that can contain any of several types

- tagged union: combines enums and unions, so you can dispatch on its tag to get one of the union types

Read from this section and they appear in order:

https://ziglang.org/documentation/master/#enum

Sad part is, is that ad hoc unions probably won’t make it into v1. That is probably one of the only feature why I like typescript. Because I can write result types in a good way without creating thousands of sub types. It’s even more important when using Promises and not having checked exceptions.
Hell yeah! After all these years it's finally here.

One thing I miss here (and admittedly I only skimmed through the post so if I missed this, please do correct me) is "ad hoc" unions.

It would be great to be able to do something like

  public Union<TypeA, TypeB> GetThing()...
Without having to declare the union first. Basically OneOf<...> but built-in and more integrated
I guess you can define

  union Union<T1, T2>(T1, T2);
Add a bunch of overloads and you'd replicate for T1|T2 syntax the equivalent mess to (Value)Tuple<...> that eventually became the backing for actual tuple syntax.
Boxed. Killed all my excitement.
You should have kept reading.

  For performance-sensitive scenarios where case types include value types, libraries can also implement the non-boxing access pattern by adding a HasValue property and TryGetValue methods. This lets the compiler implement pattern matching without boxing.
Cool, you now can implement Elm architecture inspired GUI framework in C#.

As much as I hate Microsoft, I admit they are doing great things for C#

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.

This is not "true" ad-hoc union support like TS or Scala

This is essentially "sealed interface" from Java/Kotlin or "enum" in Rust