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).
> unions enable designs that traditional hierarchies can’t express, composing any combination of existing types into a single, compiler-verified contract.
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 :)
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.
=> 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
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.
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.
.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.
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.
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.
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).
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.
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.
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.
42 comments
[ 1.8 ms ] story [ 75.8 ms ] threadAre you sure? This is a feature of OCaml but not F# IIUIR
Edit: https://github.com/fsharp/fslang-suggestions/issues/538
> unions enable designs that traditional hierarchies can’t express, composing any combination of existing types into a single, compiler-verified contract.
Yes and no. C# unions aren’t sealed types, that’s a separate feature. But they are strictly nominal - they must be formally declared:
Which isn’t at all the same as saying: It is the same as the night and day difference between tuples and nominal records.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 :)
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.
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.
=> 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
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.
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.
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)
.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).
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.
It has a bad rep because Microsoft could Microsoft as they do.
Here is visual layout if anyone is interested - https://vectree.io/c/memory-layout-tagging-and-payload-overl...
The only thing I wish now is for someone to build a functional Web framework for C#.
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.
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.
- 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
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
Without having to declare the union first. Basically OneOf<...> but built-in and more integratedAs much as I hate Microsoft, I admit they are doing great things for C#
Even outside of C#, this trend seems to show up everywhere — trying to reduce boilerplate while keeping things safe.
This is essentially "sealed interface" from Java/Kotlin or "enum" in Rust