Rendered at 01:32:15 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
glimshe 4 hours ago [-]
I love C# and in every iteration we're getting more and more features to get C-like performance in a lot of scenarios. C# does it really well because if your problem isn't performance/memory-constrained, you can ignore these features and fallback on the language's natural ease of use.
kensai 1 hours ago [-]
Do you think by now C# has left Java behind in features and performance?
gfody 9 minutes ago [-]
more than 10 years ago, yes
hirvi74 2 hours ago [-]
I daylight as a .NET dev professionally. I completely agree with what you have wrote, but I do not think C# is particularly unique in that regard. I would say many common compiled languages are on the same path, e.g., Swift, Java, Kotlin, etc.. As time progresses, I am finding it harder to justify using C# for a greenfield project.
za3faran 1 hours ago [-]
> As time progresses, I am finding it harder to justify using C# for a greenfield project.
Are you able to elaborate why? Just curious.
antonymoose 1 hours ago [-]
Not OP here - but for me it’s the open source ecosystem. Java just wins in terms of scope, scale, and stability.
I love C# the language, but the ecosystem is a ghetto.
Rohansi 34 minutes ago [-]
I see this reason a lot but what are some actual examples of what is lacking in the .NET ecosystem vs. Java?
davewritescode 10 minutes ago [-]
Large Apache ecosystem (Spark, Flink, Pinot) is completely missing and third-party SDKS (looking at you AWS) almost ALWAYS have worse SDKs. The java Kinesis consumer and producer libraries are amazing, the ones for C# are simple wrappers around the AWS APIs which means there's a few foot guns waiting for developers to run into, even if they should know better.
Amekedl 9 minutes ago [-]
Dev tools. The debugger is something for example that Microsoft ostensibly keeps to their own products, and how they totally slaughtered omnisharp.
It killed my daily csharp vscode driver couple of years ago, only now catching back up somewhat, but still unusable for bigger solutions.
That move made me gravitate towards vscodium, and avoiding csharp where possible.
Microsoft's move only recently got more understandable to me, because Cursor and others basically stole vscode to establish their "empire".
free652 7 minutes ago [-]
I mean I haven't done much in c# recently, but few examples that c# ecosystem is the subpar in quality
Kafka client library sucks, I mean it was a nightmare to make it stable and there were a few of them.
Pdfbox library
And many other libraries. If you use c# Microsoft libraries only - then you are golden. outside of that its really bad.
At this point I switched to Rust.
sedatk 3 hours ago [-]
I've waited for union types on C# so long that I don't even care about syntax anymore. Just give us something that works. So, I appreciate the effort, I know it's taken at least a decade to get it into this shape, and much thought has gone into it. Kudos to the team.
munchler 4 hours ago [-]
F# leads the way and C# slowly catches up, as always. Yet for some reason, C# still gets all the mindshare.
correct_horse 4 hours ago [-]
Haskell, OCaml, Erlang lead the way and Rust, Zig and Go get all the mindshare. I feel like its a common pattern for more experimental languages to pioneer features and other languages to copy the features and bring them to a C style syntax that the majority of devs are familiar with.
cultofmetatron 4 hours ago [-]
Rust and Zig brought new ideas for memory management that Haskell, OCaml, Erlang sidestep having garbage control. its honestly amazing to me that they managed to get the adoption they have while being so innovative. I say this as a fulltime elixir dev.
skybrian 3 hours ago [-]
Being first isn't necessarily good if you get it wrong, though. Laziness by default and Hindley-Milner type inference seem like mistakes that simply aren't going to get cleaned up. Other languages make their own mistakes too.
fwip 2 hours ago [-]
What's wrong with Hindley-Milner?
skybrian 2 hours ago [-]
Leaving out types in public API's makes type errors hard to understand. Types should be declared in the API and bidirectional type inference used in the implementation.
Eh. This causes some problems for rust. Right now you can have a function return impl Trait instead of a concrete type. Very handy - and essentially required by async functions.
But the language also requires that types have names in lots of places. For example, you can't store an 'impl Trait' in a struct. You can't make a type alias of an impl Trait. And so on. As a result, async rust can only interact with a butchered subset of the language. (You can work around this with Box<dyn Future<...>> but performance suffers.)
There's a proposal[1] to fix this. But the proposal has been under discussion for (checks watch) 7 years now. Until this lands, async remains a second class citizen in rust.
This entire problem stems from rust's early decision to requiring concrete types at interface boundaries.
I wish I could find the reference, but there was a great blog / article by a computer science academic basically saying that OO, procedural, and functional paradigms are extremes of a design space where the “middle” of its Pareto frontier was essentially unknown until recent advances.
Moreover, many functional languages are getting pseudo-procedural features via the like of “do” syntax and monads, but that this is in some sense a double abstraction over the underlying machine that is already inherently procedural.
Starting from a language that is already procedural and sprinkling some functional abstractions on top is simpler to implement and easier for humans to use and understand.
Rust especially showed that many of the supposed advantages of functional languages are not their exclusive domain, such as sum types and a powerful type system.
Note the top comment especially, which explains succinctly why functional has rather substantial downsides.
jen20 1 hours ago [-]
The idea that Erlang is experimental is pretty amusing- it’s one of the most stable platforms there is.
nozzlegear 2 hours ago [-]
I love F#. It's my go-to language and one that I work with every day. Personally I feel that IDE support (as in perf, QoL features, etc.) is the only area it lags behind C#, and outside of that it's a clear winner for everything that I want to do with it.
nullhole 3 hours ago [-]
What types of problems are better solved in F# than C#?
Is having a combination of F# and C# in a single codebase possible? Is it recommended?
Akronymus 3 hours ago [-]
Easy code is much easier in f#, a lot of the time. Hard code is usually easier in f# due to the type system helping a lot. F# is also a lot more concise.
And yes, you can combine them, but afair, only in terms project boundaries. (You can include a c# project in an f# one and vice versa). There are a few cases where it's quite useful. For example, rewriting a part of a big project in f# to leverage the imperative shell - functional core architecture. Like rewriting some part that does data processing in f#, so that you can test it easier/be more confident in correctness, while not doing a complete rewrite at once.
Sort of like rust parts in the linux kernel.
moron4hire 3 hours ago [-]
It's very possible, even encouraged when you have workloads that call for it. F# is a great functional language, so it's good for parsers, compilers, etc. The support for units of measure is also really cool, making it great for scientific computing.
rohitsriram 4 hours ago [-]
F# has had this for decades, C# is basically just slowly becoming F# with a C-style syntax. Not complaining though, most teams aren't switching languages so getting these features where people actually work is better than nothing.
caioricciuti 2 hours ago [-]
About time. TypeScript and Rust proved how much cleaner code gets when you can model "this OR that" at the type level. The real test will be whether library authors start using them in public APIs or if they stay a curiosity.
frutiger 2 hours ago [-]
Standard ML proved this 50 years ago.
hahn-kev 4 hours ago [-]
I'm glad to finally see this making it's way into C#. Not so much because I want to use unions purely in C#. But because I want to be able to define them when interfacing with other languages.
bartwe 2 hours ago [-]
As a big user and fan of c# but this is a miss, as it always boxes value types.
hahn-kev 22 minutes ago [-]
It always boxes them TODAY. Lately the team has been releasing an MVP and improving stuff (like perf) later on. I wouldn't be surprised if they do the same thing here, as noted in the article you can already work around that yourself.
algorithmsRcool 9 minutes ago [-]
But why not do it right the first time. This is an obvious performance pitfall for people that want to adopt this feature. It is bizarre to me after the last decade has been dedicated to performance improvments.
hahn-kev 3 minutes ago [-]
They do this often actually. They will likely watch how people use it and tailor the design to fix that.
As for performance, in lots of use cases it's not going to be a big deal. If you are super sensitive to performance issues then you can just wait, meanwhile everyone else gets to use the new feature. You have to start somewhere and waiting to satisfy everything usually ends up with doing nothing
zamalek 2 hours ago [-]
When I cared about C# (which is no longer the case), I was lightly involved in the discussion for this and sibling features - mostly theorycrafting exactly your ask: the JIT team very succinctly expressed extreme disinterest in adding support of any kind.
The C# compiler could do it to a degree, but there would be too many caveats to make it actually useful. Unless the JIT team has a change of heart, you're probably never going to see this.
za3faran 1 hours ago [-]
What do you use these days language-wise?
Izikiel43 2 hours ago [-]
There is the non boxing option, and this is the first iteration of the work, it's not one and done.
jdw64 3 hours ago [-]
C# is my strongest and favorite language. That said, it's frustrating that the C# framework ecosystem lacks solid options. MAUI is especially half-baked, and I'm really starting to doubt whether I should continue using XAML
mynameisash 3 hours ago [-]
C# used to be my favorite language, but having spent a lot of time in Rust using its algebraic data types + match statements + Option & Result types, then returning to C# to build a few moderately involved libraries, I'm horrified by the enums and null & error handling that I used to deal with all the time.
I knew that enums were really just named integer values and nothing more, but I had forgotten than you can build a perfectly legal enum from an integer out of the bounds of the enum's range. And a switch statement is non-exhaustive. (As I said, it had been a while since I used C# extensively.) What would have been a few lines of code in Rust turned into dozens to try to exhaustively protect against invalid input.
I know C# is a mature language that has been around for decades, but how janky everything feels comparatively really shocked me. I only very briefly played with F# about a decade ago, but my guess is that I could try to pick that up and call F# from C#, getting much better ergonomics with a combination of the two.
electroly 3 hours ago [-]
> I had forgotten than you can build a perfectly legal enum from an integer out of the bounds of the enum's range. And a switch statement is non-exhaustive
These are solved by the new feature described in the article that we're commenting on right now. They're giving us unions and exhaustive switch. Ctrl+F "canonical way to work with unions" in the article to see an example. One of the best parts about C# is they never stop bringing useful features from other languages back home to us in C#. It makes for a large language with a lot of features, but if we really want something, we'll eventually get it in C#.
at_compile_time 20 minutes ago [-]
Learning Rust really ruined C# for me. The explicitness saves you from so much defensive programming.
jdw64 3 hours ago [-]
[dead]
bob1029 3 hours ago [-]
Winforms is better than ever. You can use it with .NET10 and WebView2 is a thing now.
makotech221 3 hours ago [-]
Winforms, wpf, blazor, maui, avalonia, what are you talking about
Salgat 3 hours ago [-]
What I don't get is why Java doesn't get dogged for desktop UI like C# does.
blanched 3 hours ago [-]
Because Microsoft pushes C#/dotnet as the preferred way to write UI on Windows.
jdw64 3 hours ago [-]
Alright. I'm actually fine with WinForms and WPF since my factory floor codes depend on them. But the reality is they aren't expressive enough for modern UIs. XAML is an issue, and WPF is boilerplate hell. But then Blazor is too heavy, MAUI is broken and buggy, Avalonia is underwhelming, and WinUI 3 is an absolute nightmare.
Rohansi 26 minutes ago [-]
> But the reality is they aren't expressive enough for modern UIs
Other than web tech, what actually is expressive enough?
sedatk 3 hours ago [-]
> Avalonia is underwhelming
How? Can you elaborate?
vitally3643 3 hours ago [-]
Not OP, but I've found Avalonia to be pretty much a direct replacement for WinForms. I mean that both as a compliment and a deserved insult. It's not the WinForms we wanted, but it is the one we deserve.
More seriously, it has all the strengths and weaknesses of WinForms and feels about exactly as unfinished and rough as WinForms. I still have to implement custom widgets that i would have expected to be included out of the box. It's nice that it's cross-platform, though with all the rough edges that cross-platform .net still has. It really, truly feels exactly like every C# UI framework I've ever used in the last 20 years: almost good, not quite finished, and takes an amount of effort that is just unreasonable compared to any other language/framework of any age.
I've been a C# dev for most of my career. I have more fun writing UIs from scratch by drawing individual pixels in C++ than any C# UI.
sedatk 2 hours ago [-]
I'm truly surprised that it feels that underdeveloped. They market Avalonia as a direct replacement for WPF too. So, I'd expect it at least match WPF to be fair.
vitally3643 2 hours ago [-]
I would argue quite fervently that WinForms is more than a match for WPF. The only thing worse than WPF is UWP. We don't talk about UWP.
jdw64 3 hours ago [-]
[dead]
moomin 4 hours ago [-]
AFAICT, this means you won’t be able to define Either<string, string>, which is definitely a thing you sometimes want to do.
sheept 4 hours ago [-]
It seems like if you wrap both in a record then it should be possible:
public record Left<T>(T Value);
public record Right<T>(T Value);
public union Either<L, R>(Left<L>, Right<R>);
jzebedee 4 hours ago [-]
C# is strongly-typed, not stringly-typed. The point of the union is to list possible outcomes as defined through their respective types.
The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:
Where :yes and :no are memory-efficient symbols, not strings.
goto11 2 hours ago [-]
You cant have a `type Foo = String | Strimg` in Haskell either.
goto11 2 hours ago [-]
Well it is a type union. The union of string and string is just string.
throw1234567891 4 hours ago [-]
but can you define T1 and T2 of string, then use Either<T1, T2>?
2 hours ago [-]
eterm 3 hours ago [-]
Could you be clearer about what you mean, since string is a sealed type in C#, so what exactly do you mean T1 and T2 of string?
rawling 3 hours ago [-]
A record wrapping a string, indicating what the string represents, so you can't mix it up with a different thing also represented by a string.
eterm 3 hours ago [-]
Yes, you can have two different record types which both wrap a string value.
As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:
var fileResult = Helpers.ReadFile(@"c:\temp\test.txt");
Console.WriteLine("Extracted:");
Console.WriteLine(Helpers.ExtractString(fileResult));
public record FileRead(string value);
public record FileError(string value);
public union FileResult(FileError, FileRead);
public static class Helpers
{
public static FileResult ReadFile(string fileName)
{
try
{
var fileResult = System.IO.File.ReadAllText(fileName);
return new FileRead(fileResult);
}
catch (Exception ex)
{
return new FileError(ex.Message);
}
}
public static string ExtractString(FileResult result)
{
return result switch
{
FileError err => $"An Error occured: {err.value}",
FileRead content => content.value,
_ => throw new NotImplementedException()
};
}
}
Now, such an example would be an odd way to do things ( particuarly because we're not actually avoiding the try/catch inside ), but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.
It's more useful implemented a level deeper, so that the exception is never raised and caught, because exceptions aren't particularly cheap in .NET.
cubefox 2 hours ago [-]
Most people don't know that there are fundamentally two different kinds of "union" types: "tagged unions" and "untagged unions". Now .NET is introducing tagged unions, but unfortunately they stick to the popular tradition of calling them just "unions", which greatly adds to the confusion.
To clarify, these tagged unions are fundamentally different from the untagged unions that can be found in languages like Typescript or Scala 3.
Tagged unions (also called "discriminated unions" or "sum types") are algebraic data types. They act as wrappers, via special constructors, for two (or more) disjoint types and values. So a tagged union acts like a bag that has two (or more) labelled ("tagged") slots, where each slot has exactly one type and exactly one of these slot can take a value.
Untagged unions are set theoretic (rather than algebraic) data types. They don't require wrapping via a special constructor. They behave like the logical OR. They are not disjoint slots of a separate construct. A variable or function x with the type "Foo | Bar" can be of type Foo, or Bar, or both. To access a Foo method on x, one has to first perform a typecheck for Foo, otherwise the compiler will refuse the method call (since x might only have the type Bar which would produce an exception). If a variable is of type A, it is also of type A|B ("A or B"). There are also intersection types (A&B) which indicate that something has both types rather than at least one, and complement/negation types (~A indicates that something is of any type except A). Though the latter are not implemented in any major language so far.
troad 47 minutes ago [-]
Underlyingly, all unions are the same concept, exposed by C's `union` keyword, namely overlapping memory (as opposed to `struct`s, which are sequential memory). You can add a discriminator tag to your unions, your call. If it is impossible for there to be ambiguity (e.g. you track the state somewhere else, and you're dealing with a large array), it may be redundant / memory inefficient to tag every item.
Having these tags be a language construct is just a DX feature on top of unions. A very handy one, but it doesn't make tagged and untagged unions spring from different theoretical universes. I enjoy the ADT / set-theoretic debate as much as the next PL nerd, but theory ought to conform to reality, not vice versa.
deadeye 4 hours ago [-]
I wish the syntax looked more like typescript. This will confuse my eyes for a while.
ameliaquining 3 hours ago [-]
It works quite differently from TypeScript, so the syntax used there wouldn't have worked.
LAC-Tech 3 hours ago [-]
Wow 2016 would have loved this news.
4 hours ago [-]
SuperV1234 3 hours ago [-]
Boxed, and needs complex incantations to avoid the boxing. Meh.
antonvs 3 hours ago [-]
Did Anders Hejlsberg die, or something?
Quarrelsome 4 hours ago [-]
I mean yes, but also: uh-oh.
I'm looking forward to reading some code that is even more confusing than the code I'm already reading.
Not entirely convinced that I see the usecase that makes up for the potential madness.
zoogeny 3 hours ago [-]
This is a classic debate in programming, literally:
2001: "Beating the Averages" (Paul Graham) [1]
2006: "Can Your Programming Language Do This?" (Joel Spolsky) [2]
Both of these articles argue for the thesis that programmers that have been deprived of certain language features often argue that they don't need those features since they are already comfortable working around the lack of said features.
It's a fancy way of arguing: you don't know what you're missing because you've never had it. Or, don't knock it until you try it.
Consider, is your argument a) I've never used it and don't see a need for it, or b) I've used it before and didn't get any benefit?
I can already do functional programming like map/reduce in C# tho. Not sure what the LISP argument is. Spolsky was saying there's a perf benefit in there somewhere but I'm not seeing how unions give me that.
zoogeny 3 hours ago [-]
You have at least two options:
1. Argue from ignorance. Never try unions in any other programming languages and completely disallow their use in C# codebases that you participate in.
2. Try them out and adopt an informed opinion.
You may even choose to remain in ignorance until someone wastes their own time trying to convince you. But it isn't my job or desire to teach someone who won't put in the effort to learn for themselves.
3 hours ago [-]
munchler 4 hours ago [-]
Unions are simpler than subclasses and more powerful than enums, so the use cases are plentiful. This should reduce the proliferation of verbose class hierarchies in C#. Algebraic data types (i.e. records and unions) can usually express domain models much more succinctly than traditional OO.
Quarrelsome 4 hours ago [-]
> so the use cases are plentiful
such as?
> This should reduce the proliferation of verbose class hierarchies in C#
So just as an alternative for class hierarchies? I mean good people already balance that by having a preference for composition.
munchler 4 hours ago [-]
Simple example:
type Expr =
| Primitive of int
| Addition of (Expr * Expr)
| Subtraction of (Expr * Expr)
| Negation of Expr
Quarrelsome 4 hours ago [-]
Isn't that just Func<int> ?
afdbcreid 3 hours ago [-]
Really not. You can, of course, having instead a delegate to evaluate the expression. But then that's all you can do. You can't pretty-print it, for example, or optimize it, or whatever.
3 hours ago [-]
LeFantome 4 hours ago [-]
“Compoision”. A typo I know but it would be a word describing what goes wrong with class hierarchies.
Sharlin 4 hours ago [-]
Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.
Quarrelsome 4 hours ago [-]
ok, so what problems do they help me solve that I can't already solve? Is it just that we can make code more concise or am I missing a trick somewhere?
rspeele 29 minutes ago [-]
Object-oriented polymorphism (interfaces, inheritance) is for when you have a fixed set of methods to implement but an unbounded set of types that may want to implement them. Unbounded meaning there could be thousands of different implementations in downstream code that the author of the base class never foresaw. Very successful, well designed examples in .NET include: System.IO.Stream, System.Windows.Forms.Control, IEnumerable<T>.
As a downstream consumer, you cannot change the methods, but you can add a subtype. When you subtype an abstract class or an interface, the compiler does not let you proceed until you have implemented all the methods.
Discriminated unions are for the exact opposite situation, when you have a fixed set of subtypes, but unbounded set of methods to implement on them. As a downstream consumer, you cannot add a subtype, but you can add a new method. When you write a new method, the compiler does not let you proceed until you have handled all the subtypes.
The best example is abstract syntax trees, the data types that represent expressions and statements in a programming language. "Expression" breaks down into cases: integer literal, string literal, variable name, binary operations like add(expr1,expr2), unary operations like negate(expr), function call(functionName, exprs), etc.
Clearly all of these expression subtypes should belong to a base type `Expression`. But what methods do you put on `Expression`? If you're writing a compiler, you have to walk this syntax tree many times for very different purposes. First you might do a pass on it where you "de-sugar" syntax, then another pass where you type-check it and resolve names in the code, then another pass where you generate assembly code from it. Perhaps your compiler even supports different backends so you have a code-gen path for x86, another for ARM, etc. You'll likely want a pretty-printer so you can do automatic reformatting, maybe you want linting support, etc.
If you look at all those concerns and say that each subtype of `Expression` must implement methods for each one, then you end up with untenable code organization. Every expression subtype now has a huge stack of methods to implement all in one file, dealing with stuff from totally different layers of the compiler. It's a mess.
It's much cleaner to have the "shape" of the expression defined in one place without all that clutter, and then in each of those areas of the code you can write methods that consume expressions however they need, so each of those separate concerns lives in its own silo.
------------------------------------------------
If you're an old hand at OO you may be familiar with its actual answer to this problem, the "Visitor" pattern. See System.Linq.Expressions.ExpressionVisitor. However, once you've used a language with good union and pattern matching support, this feels like a clunky hack. Basically the mirror image of a language without real object orientation imitating it by passing around closures and structs-of-closures.
bigstrat2003 4 hours ago [-]
I think "what problems do they solve that I can't already solve" is the wrong way to look at it. After all, ultimately most language features are just syntactic sugar - you could implement for loops with goto, but it would be a lot less pleasant. I think that unions aren't strictly necessary, but they are a very pleasant to use way of differentiating between different, but related, types of value.
Quarrelsome 4 hours ago [-]
Ok. I'm just trying to understand what code I'm replacing with them. Like I wanna see the before and after in order to gain the same level of excitment as other people seem to have for them.
Often the explanations just seem rather abstract which makes it harder to appreciate the win, versus the hideous sort of code that might appear when they're misused.
airstrike 3 hours ago [-]
They are so fundamental to the way I write code I can't imagine ever using a language that does not support them.
"Make invalid states unrepresentable."
arwhatever 14 minutes ago [-]
I might suggest that anyone who wants to make it concrete to go through the article
while visiting https://dotnetfiddle.net and typing the code samples in, experimenting with what manner of changes and additions to the code cause the compilation to fail, and considering how you would leverage those abilities in your everyday development work.
I think this would be even more powerful if you then come back and re-read some of the pro-Union comments in this very thread.
speed_spread 3 hours ago [-]
The value is realized when you have both discriminated union types _and_ language pattern matching (not regex). Then it's not just a way to structure data but a way to think about how to process it.
JCTheDenthog 4 hours ago [-]
Simple example that I use often when writing API clients:
In current C# I usually do something like
public class ApiResponse<T>
{
public T? Response { get; set; }
public bool IsSuccessful { get; set; }
public ErrorResponse Error { get; set; }
}
This means I have to check that IsSuccessful is true (and/or that Response is not null). But more importantly, it means my imbecile coworkers who never read my documentation need to do so as well otherwise they're going to have a null reference exception in prod because they never actually test their garbage before pushing it to prod. And I get pulled into a 4 hour meeting to debug and solve the issue as a result.
With union types, I can return a union of the types T and ErrorResponse and save myself massive headaches.
Quarrelsome 3 hours ago [-]
I think I get it but I'm not really sure what I'm gaining over exception types. With an intelligent use of exceptions I can easily specify the happy path and all the error paths separately which seems really nice to me, because usually the behaviour between those two outcomes is rather different.
Akronymus 2 hours ago [-]
> I think I get it but I'm not really sure what I'm gaining over exception types. With an intelligent use of exceptions I can easily specify the happy path and all the error paths separately which seems really nice to me, ...
Until your coworker comes along and accidentally refactors the code to skip the exception catching and it suddenly blows up prod.
With tagged unions you can't accidentally dereference to the underlying value without checking if it's actually proper data first.
JCTheDenthog 3 hours ago [-]
Exceptions are significantly slower than normal control flow in C# (about 10,000 times slower). It's also pretty non-idiomatic in both C# and most other languages I've worked in to use exceptions instead of a switch statement or similar to handle an HTTP error code. Also there can be multiple possible non-error responses from an endpoint you need to differentiate between, and exceptions would make zero sense in that case.
cubefox 2 hours ago [-]
> Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.
"Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has them.
vips7L 4 hours ago [-]
Union/sum types are generally a good thing. Even Java added them. They tend to be worth “the madness”. Now the rest of all the crazy C# features might be a different question.
dgellow 4 hours ago [-]
What features do you see as crazy?
munchler 4 hours ago [-]
All the weird cruft around nullability, for starters. Once again confirming that allowing null references is usually a mistake.
dgellow 4 hours ago [-]
Do you mean the implicit nullable types? Now that you can make nullable explicit instead I really don’t have much issues with it. It is part of the type system, as it should, and you have null coalescing operators. Is it still problematic or are you dealing with older codebases where you cannot set the nullable pragma?
munchler 1 hours ago [-]
Yes, all that stuff. I try to stick to F# where no special syntax is required for missing values (via Option<T>).
vips7L 3 hours ago [-]
Maybe not crazy but the language just has a really broad surface. I find it to be like the Scala of the OO world.
oompydoompy74 4 hours ago [-]
You don’t see the use case for… unions? I’ve got to stop reading the comments. It’s bad for my health.
adjejmxbdjdn 4 hours ago [-]
I love discriminated unions.
The problem with C# is that it’s so overloaded with features.
If you come from one codebase to another codebase by a different team it’s close to learning a completely new language, but worse, there is no documentation I can find that will teach me only about that language.
Throw in all the versioning issues and the fact that .Net shops aren’t great about updating to the latest versions, especially because versions, although technologically separated from Visual Studio, are still culturally tied to it, and trying to break that coupling causes all kinds of weird challenges to solve.
Then stuff like extensions means your private codebase or a 3rd party lib may have added native looking functionality that’s not part of the language but looks like it is.
Finally, keywords and operators are terribly overloaded in C# at this point, where a keyword can have completely different meanings based on what it’s surrounded by.
LLMs are a huge help here, since you can point to a line of code and ask them to figure it out, but it still makes the process of navigating a C# codebase extremely challenging.
So I can see why someone may be unhappy to see yet another feature. It’s not just this one feature. It’s the 100s of other features that are hard to even identify.
paddim8 4 hours ago [-]
I am all for minimalism but "If you come from one codebase to another codebase by a different team it’s close to learning a completely new language" I really don't agree. It's not that big. Just sounds like a skill issue
Quarrelsome 4 hours ago [-]
none of that applies to my position. I have an appreciation for almost all of C# and am comfortable in the framework. I just want to know what situations would be better suited to using them than traditional approaches.
I get there's an .Either pattern when chaining function calls so you don't have to do weird typing to return errors, but I'm using exceptions for that anyway, so the return type isn't an issue.
itishappy 2 hours ago [-]
The Result pattern can be a lot more ergonomic than exceptions.
Microsoft C# guidelines recommend try-parse (which is just the Result pattern, albeit somewhat cludgy with no unions) over exceptions.
the result pattern doesn't force you to handle the exception though. You can just discard the result.
Quarrelsome 4 hours ago [-]
thanks for helping.
weinzierl 4 hours ago [-]
A common use case for the sum type is to define a Result (or Either) type. Now, C# not having checked exceptions is not as much in need for one as Java is, but I could still
imagine it being useful for stream
like constructs.
Quarrelsome 4 hours ago [-]
yeah this is the one I've considered as being mildly compelling. But don't we lose the fun of having exception handling as separate to the happy path?
andix 3 hours ago [-]
I've never been confused by language features. Usually the architecture or extreme indirection of the code is the confusing part.
le-mark 4 hours ago [-]
I used to see some excitement around .net core several years ago. I haven’t heard or seen much in the wild. Is anyone using .net on systems other than windows nowadays?
lol768 4 hours ago [-]
Yes; many (Alpine/Debian) containers in K8s on GKE for production rail ticketing infra in the UK.
There's not tons of noise being made because for the most part it all, Just Works and that's fairly boring. Perf, memory usage etc gets better every release. As an ecosystem, I'm pretty happy with it. I reach for other languages for smaller microservices.
za3faran 1 hours ago [-]
What's preventing you from using C# for smaller microservices? And what do you reach for?
gib444 4 hours ago [-]
> rail ticketing infra in the UK
You mean Raileasy? Or RDG too? (Just curious about the stack of the wider rail tech infra)
dgellow 4 hours ago [-]
It’s huge in the game dev world, with Unity and Godot. .net also had a reasonable community on mobile for a while thanks to Xamarin, but I cannot imagine that many people using it for new mobile projects in 2026 (outside of game dev I mean).
It’s a very decent language (I mean C#) and runtime, I wish it had more market share in the startup world.
smlavine 4 hours ago [-]
An enterprise shop I co-op'd at was porting one of their apps from Xamarin to MAUI when I worked there, but certainly it doesn't have much mindshare (if any) amongst SE undergrads at my university.
unethical_ban 4 hours ago [-]
Someone I know who works with .net says that there is still no replacement for full Visual Studio for development, which is Windows only.
rjbwork 31 minutes ago [-]
I used to think this. Hopped to rider 4 years ago and haven't missed it except for .sqlproj development.
dgellow 3 hours ago [-]
Rider is the replacement, unless they are doing really specific (like WinUI2/UWP)
Rohansi 2 hours ago [-]
VS Code is also manageable. Or the CLI tools, if that's your thing.
Rider is definitely the most equivalent to full Visual Studio though.
Rohansi 3 hours ago [-]
Unity is still using Mono these days which is missing basically all of the C# and .NET improvements from the past... 10 years now?
Godot was using Mono too but has since switched to .NET in version 4.
Still a great language and I hope Unity can hit their target to switch to .NET soon!
dgellow 3 hours ago [-]
Damn, I assumed they had switched to .net core, I cannot believe they are still stuck on mono. Thanks for the correction
bel8 4 hours ago [-]
I consulted for multiple enterprise C# projects in the last 5 years. At least two of them are 1mil+ lines of code each.
All of them run in Linux servers.
Some of them were ported from PHP and Python to C#.
Plus LLMs thrive in strongly typed languages.
Which means C# will keep being very strong in enterprise too. Not only in games where it reigns a large chunk of the market share.
4 hours ago [-]
forgotaccount3 4 hours ago [-]
Yes, lambda's and our dev's use mac's so it enables that. We deploy some apps to some unix based server as well but the company is mostly windows servers anyway.
yread 4 hours ago [-]
Wwwuuuuuaaahhhhh! (making a big wild excited noise using asp.net core exclusively on Linux servers since 2017)
b65e8bee43c2ed0 4 hours ago [-]
it was an obvious marketing campaign. back then core and blazor were shilled relentlessly, and the artificial excitement died the moment MS moved on to shill vscode and typescript.
companies spend a lot on marketing, and it's not just ads.
Are you able to elaborate why? Just curious.
I love C# the language, but the ecosystem is a ghetto.
It killed my daily csharp vscode driver couple of years ago, only now catching back up somewhat, but still unusable for bigger solutions.
That move made me gravitate towards vscodium, and avoiding csharp where possible.
Microsoft's move only recently got more understandable to me, because Cursor and others basically stole vscode to establish their "empire".
Kafka client library sucks, I mean it was a nightmare to make it stable and there were a few of them.
Pdfbox library
And many other libraries. If you use c# Microsoft libraries only - then you are golden. outside of that its really bad.
At this point I switched to Rust.
https://jimmyhmiller.com/easiest-way-to-build-type-checker
But the language also requires that types have names in lots of places. For example, you can't store an 'impl Trait' in a struct. You can't make a type alias of an impl Trait. And so on. As a result, async rust can only interact with a butchered subset of the language. (You can work around this with Box<dyn Future<...>> but performance suffers.)
There's a proposal[1] to fix this. But the proposal has been under discussion for (checks watch) 7 years now. Until this lands, async remains a second class citizen in rust.
This entire problem stems from rust's early decision to requiring concrete types at interface boundaries.
https://github.com/rust-lang/rust/issues/63063
Moreover, many functional languages are getting pseudo-procedural features via the like of “do” syntax and monads, but that this is in some sense a double abstraction over the underlying machine that is already inherently procedural.
Starting from a language that is already procedural and sprinkling some functional abstractions on top is simpler to implement and easier for humans to use and understand.
Rust especially showed that many of the supposed advantages of functional languages are not their exclusive domain, such as sum types and a powerful type system.
Update: Hah! ChatGPT found it: https://news.ycombinator.com/item?id=21280429
Note the top comment especially, which explains succinctly why functional has rather substantial downsides.
Is having a combination of F# and C# in a single codebase possible? Is it recommended?
And yes, you can combine them, but afair, only in terms project boundaries. (You can include a c# project in an f# one and vice versa). There are a few cases where it's quite useful. For example, rewriting a part of a big project in f# to leverage the imperative shell - functional core architecture. Like rewriting some part that does data processing in f#, so that you can test it easier/be more confident in correctness, while not doing a complete rewrite at once.
Sort of like rust parts in the linux kernel.
As for performance, in lots of use cases it's not going to be a big deal. If you are super sensitive to performance issues then you can just wait, meanwhile everyone else gets to use the new feature. You have to start somewhere and waiting to satisfy everything usually ends up with doing nothing
The C# compiler could do it to a degree, but there would be too many caveats to make it actually useful. Unless the JIT team has a change of heart, you're probably never going to see this.
I knew that enums were really just named integer values and nothing more, but I had forgotten than you can build a perfectly legal enum from an integer out of the bounds of the enum's range. And a switch statement is non-exhaustive. (As I said, it had been a while since I used C# extensively.) What would have been a few lines of code in Rust turned into dozens to try to exhaustively protect against invalid input.
I know C# is a mature language that has been around for decades, but how janky everything feels comparatively really shocked me. I only very briefly played with F# about a decade ago, but my guess is that I could try to pick that up and call F# from C#, getting much better ergonomics with a combination of the two.
These are solved by the new feature described in the article that we're commenting on right now. They're giving us unions and exhaustive switch. Ctrl+F "canonical way to work with unions" in the article to see an example. One of the best parts about C# is they never stop bringing useful features from other languages back home to us in C#. It makes for a large language with a lot of features, but if we really want something, we'll eventually get it in C#.
Other than web tech, what actually is expressive enough?
How? Can you elaborate?
More seriously, it has all the strengths and weaknesses of WinForms and feels about exactly as unfinished and rough as WinForms. I still have to implement custom widgets that i would have expected to be included out of the box. It's nice that it's cross-platform, though with all the rough edges that cross-platform .net still has. It really, truly feels exactly like every C# UI framework I've ever used in the last 20 years: almost good, not quite finished, and takes an amount of effort that is just unreasonable compared to any other language/framework of any age.
I've been a C# dev for most of my career. I have more fun writing UIs from scratch by drawing individual pixels in C++ than any C# UI.
The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.
[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:
Of course you'd need a trivial parser, though this is probably a good idea for any string type: Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example: Where :yes and :no are memory-efficient symbols, not strings.As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:
Now, such an example would be an odd way to do things ( particuarly because we're not actually avoiding the try/catch inside ), but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.It's more useful implemented a level deeper, so that the exception is never raised and caught, because exceptions aren't particularly cheap in .NET.
To clarify, these tagged unions are fundamentally different from the untagged unions that can be found in languages like Typescript or Scala 3.
Tagged unions (also called "discriminated unions" or "sum types") are algebraic data types. They act as wrappers, via special constructors, for two (or more) disjoint types and values. So a tagged union acts like a bag that has two (or more) labelled ("tagged") slots, where each slot has exactly one type and exactly one of these slot can take a value.
Untagged unions are set theoretic (rather than algebraic) data types. They don't require wrapping via a special constructor. They behave like the logical OR. They are not disjoint slots of a separate construct. A variable or function x with the type "Foo | Bar" can be of type Foo, or Bar, or both. To access a Foo method on x, one has to first perform a typecheck for Foo, otherwise the compiler will refuse the method call (since x might only have the type Bar which would produce an exception). If a variable is of type A, it is also of type A|B ("A or B"). There are also intersection types (A&B) which indicate that something has both types rather than at least one, and complement/negation types (~A indicates that something is of any type except A). Though the latter are not implemented in any major language so far.
Having these tags be a language construct is just a DX feature on top of unions. A very handy one, but it doesn't make tagged and untagged unions spring from different theoretical universes. I enjoy the ADT / set-theoretic debate as much as the next PL nerd, but theory ought to conform to reality, not vice versa.
Not entirely convinced that I see the usecase that makes up for the potential madness.
2001: "Beating the Averages" (Paul Graham) [1]
2006: "Can Your Programming Language Do This?" (Joel Spolsky) [2]
Both of these articles argue for the thesis that programmers that have been deprived of certain language features often argue that they don't need those features since they are already comfortable working around the lack of said features.
It's a fancy way of arguing: you don't know what you're missing because you've never had it. Or, don't knock it until you try it.
Consider, is your argument a) I've never used it and don't see a need for it, or b) I've used it before and didn't get any benefit?
1. https://paulgraham.com/avg.html?viewfullsite=1
2. https://www.joelonsoftware.com/2006/08/01/can-your-programmi...
1. Argue from ignorance. Never try unions in any other programming languages and completely disallow their use in C# codebases that you participate in.
2. Try them out and adopt an informed opinion.
You may even choose to remain in ignorance until someone wastes their own time trying to convince you. But it isn't my job or desire to teach someone who won't put in the effort to learn for themselves.
such as?
> This should reduce the proliferation of verbose class hierarchies in C#
So just as an alternative for class hierarchies? I mean good people already balance that by having a preference for composition.
As a downstream consumer, you cannot change the methods, but you can add a subtype. When you subtype an abstract class or an interface, the compiler does not let you proceed until you have implemented all the methods.
Discriminated unions are for the exact opposite situation, when you have a fixed set of subtypes, but unbounded set of methods to implement on them. As a downstream consumer, you cannot add a subtype, but you can add a new method. When you write a new method, the compiler does not let you proceed until you have handled all the subtypes.
The best example is abstract syntax trees, the data types that represent expressions and statements in a programming language. "Expression" breaks down into cases: integer literal, string literal, variable name, binary operations like add(expr1,expr2), unary operations like negate(expr), function call(functionName, exprs), etc.
Clearly all of these expression subtypes should belong to a base type `Expression`. But what methods do you put on `Expression`? If you're writing a compiler, you have to walk this syntax tree many times for very different purposes. First you might do a pass on it where you "de-sugar" syntax, then another pass where you type-check it and resolve names in the code, then another pass where you generate assembly code from it. Perhaps your compiler even supports different backends so you have a code-gen path for x86, another for ARM, etc. You'll likely want a pretty-printer so you can do automatic reformatting, maybe you want linting support, etc.
If you look at all those concerns and say that each subtype of `Expression` must implement methods for each one, then you end up with untenable code organization. Every expression subtype now has a huge stack of methods to implement all in one file, dealing with stuff from totally different layers of the compiler. It's a mess.
It's much cleaner to have the "shape" of the expression defined in one place without all that clutter, and then in each of those areas of the code you can write methods that consume expressions however they need, so each of those separate concerns lives in its own silo.
------------------------------------------------
If you're an old hand at OO you may be familiar with its actual answer to this problem, the "Visitor" pattern. See System.Linq.Expressions.ExpressionVisitor. However, once you've used a language with good union and pattern matching support, this feels like a clunky hack. Basically the mirror image of a language without real object orientation imitating it by passing around closures and structs-of-closures.
Often the explanations just seem rather abstract which makes it harder to appreciate the win, versus the hideous sort of code that might appear when they're misused.
"Make invalid states unrepresentable."
https://fsharpforfunandprofit.com/posts/designing-with-types...
while visiting https://dotnetfiddle.net and typing the code samples in, experimenting with what manner of changes and additions to the code cause the compilation to fail, and considering how you would leverage those abilities in your everyday development work.
I think this would be even more powerful if you then come back and re-read some of the pro-Union comments in this very thread.
In current C# I usually do something like
public class ApiResponse<T> { public T? Response { get; set; } public bool IsSuccessful { get; set; } public ErrorResponse Error { get; set; } }
This means I have to check that IsSuccessful is true (and/or that Response is not null). But more importantly, it means my imbecile coworkers who never read my documentation need to do so as well otherwise they're going to have a null reference exception in prod because they never actually test their garbage before pushing it to prod. And I get pulled into a 4 hour meeting to debug and solve the issue as a result.
With union types, I can return a union of the types T and ErrorResponse and save myself massive headaches.
Until your coworker comes along and accidentally refactors the code to skip the exception catching and it suddenly blows up prod.
With tagged unions you can't accidentally dereference to the underlying value without checking if it's actually proper data first.
"Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has them.
The problem with C# is that it’s so overloaded with features.
If you come from one codebase to another codebase by a different team it’s close to learning a completely new language, but worse, there is no documentation I can find that will teach me only about that language.
Throw in all the versioning issues and the fact that .Net shops aren’t great about updating to the latest versions, especially because versions, although technologically separated from Visual Studio, are still culturally tied to it, and trying to break that coupling causes all kinds of weird challenges to solve.
Then stuff like extensions means your private codebase or a 3rd party lib may have added native looking functionality that’s not part of the language but looks like it is.
Finally, keywords and operators are terribly overloaded in C# at this point, where a keyword can have completely different meanings based on what it’s surrounded by.
LLMs are a huge help here, since you can point to a line of code and ask them to figure it out, but it still makes the process of navigating a C# codebase extremely challenging.
So I can see why someone may be unhappy to see yet another feature. It’s not just this one feature. It’s the 100s of other features that are hard to even identify.
I get there's an .Either pattern when chaining function calls so you don't have to do weird typing to return errors, but I'm using exceptions for that anyway, so the return type isn't an issue.
Microsoft C# guidelines recommend try-parse (which is just the Result pattern, albeit somewhat cludgy with no unions) over exceptions.
https://learn.microsoft.com/en-us/dotnet/standard/design-gui...
There's not tons of noise being made because for the most part it all, Just Works and that's fairly boring. Perf, memory usage etc gets better every release. As an ecosystem, I'm pretty happy with it. I reach for other languages for smaller microservices.
You mean Raileasy? Or RDG too? (Just curious about the stack of the wider rail tech infra)
It’s a very decent language (I mean C#) and runtime, I wish it had more market share in the startup world.
Rider is definitely the most equivalent to full Visual Studio though.
Godot was using Mono too but has since switched to .NET in version 4.
Still a great language and I hope Unity can hit their target to switch to .NET soon!
All of them run in Linux servers.
Some of them were ported from PHP and Python to C#.
Plus LLMs thrive in strongly typed languages.
Which means C# will keep being very strong in enterprise too. Not only in games where it reigns a large chunk of the market share.
companies spend a lot on marketing, and it's not just ads.