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".
I also find serialization/deserialization to be weak in .NET. Third-party Newtonsoft was king for years, then Microsoft released System.Text.Json. Years later, it lacks feature parity, including an easy way to debug like Newtonsoft did.
It is not, Java is like C, C++, JavaScript and co.
There are many implementations, the language and runtime are evolving by industry partners, you get bare metal implementations with real time GC like PTC and Aicas (doing AOT for decades), JIT caches, cloud based JIT compilers (OpenJ9 and Azul), pauseless GC, an LLVM like compiler development framework (Graal),....
There are industry standards like Microprofile and Jakarta EE, which several vendors base their frameworks and application servers on.
And a mobile phone platform, which while isn't proper Java compliant, has enough pieces into it that makes it easier to integrate Java code and libraries, than using Xamarin.
Microsoft actually bothered with ECMA during the early days, however it hasn't been updated since C# 7.3, .NET Framework 4.8.
I know it's not a popular opinion but I don't see why they should bother. There are already multiple implementations that serve different use cases, but they are slowly converging into one. There are more languages+runtimes which do not have formal specifications than those that do, and that's okay! Fragmentation is not a good thing.
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.
What I meant is that I try to be pragmatic when writing applications. Most languages solve most problems with relatively minor differences between them.
Of course, some languages excel in areas few or others do not. When it comes to C#, I struggle to justify when it is the “right tool for the right job.” If I were building a game in Unity, C# makes sense as it’s the only option. But if I were to build, I do not know, a FizzBuzz Widget, why use C# over any other language?
The only answer I can usually come up with is language familiarity. My employer’s answer to that question would be, “Because C# is a Microsoft product.”
For example, GC escape analysis, automatic lock elision, devirtualization, tiered compilation are fairly recent features in C#, and likely not as mature/powerful.
Or C# has generic specialization, so if your generic param is a stuct, you get a separate implementation, while Java generics work via type deletion.
But in C# you have a ton more synchronization primitives, value types, methods are non-virtual by default etc.
This usually means that expertly crafted C# code can be faster than Java (and more importantly, you can trust the compiler to do the right thing), while if you wrote it exactly like Java, you'd probably end up with slower code.
That's not the case for some time already, at worst you get similar performance with Java and with a little effort you can get significantly better performance.
Operator overloading increases consistency. Instead of having
int a = b + c;
CustomNumberType x = y.add(z);
you have int a = b + c;
CustomNumberType x = y + z;But not having operator overloading when dealing with vectors is maddening.
Person x;
Job y;
CustomType z = x + y;
WTF is Z?Is the argument, anyway, I support operator overloading.
Hopefully a type error, because no sane programmer would implement addition like this. Obviously an insane programmer could, but that’s not the fault of operator overloading. The following code is exactly as confusing:
Person x;
Job y;
CustomType z = add(x, y);Take a person, add a job, you get an Employee or EmployedPerson. Person.add(job).
I agree overloading can create footguns, but domain concepts should make a lot of sense in context when basic arithmetic operations are performed on them. Party = Meeting + Booze.
Custom operators are a big assistance for DSLs, and overloading can also aide their creation and elegance. Part of the “awesome but don’t be a jerk about it” toolbox.
One of the reasons why it took off.
Turns out they really want to have plenty of Java development on Azure as well.
https://javapro.io/2026/03/05/java-25-and-the-new-age-of-per...
https://docs.oracle.com/en/java/javase/26/core/virtual-threa...
Associating virtual threads with "legacy heavy weight threads" is a fundamental misunderstanding
> That’s why Swift, Rust and Typescript all chose async/await for concurrency.
And Java chose to join Team Go/Erlang. At the end of the day, async/await is just syntactic sugar for futures/promises, which are essentially a way out of callback hell.
Besides, Rust and Typescript aren't good examples here: a green-thread scheduler (a runtime component) contradicts Rust's philosophy, while Typescript is inherently constrained by Javascript.
https://github.com/dotnet/runtimelab/issues/2398
Which ended with,
> We have chosen to place the green threads experiment on hold and instead keep improving the existing (async/await) model for developing asynchronous code in .NET. This decision is primarily due to concerns about introducing a new programming model. We can likely provide more value to our users by improving the async model we already have. We will continue to monitor industry trends in this field.
Now three years later we have,
https://github.com/dotnet/core/blob/main/release-notes/11.0/...
> Runtime async is a major runtime feature in .NET 11 that introduces new runtime-level infrastructure for async methods. The goal is to improve tooling and performance for async-heavy codepaths. For more details and to track progress, see the Runtime Async epic issue.
So it's going to be an internal implementation change -- that is, it generates the MSIL code that's generate when a developer writes async/await code, and won't require changes to how a developer writes async/await code?
No, they won't. C# already got itself into a corner with 32 bit arrays and 32 bit spans. And if unions are introduced as reference only that will never be fixed due to binary compatibility requirement.
Java’s Optional sucks compared to how C# (and Kotlin) implement support for nullable types. C#’s async/await syntax is better than… however the hell Java says to implement asynchronous calls now (Thread? CompletableFuture? idk, I never figured it out). ffs, Java doesn’t even have support for string templates yet — they added it as a JDK preview feature (JDK 21?) and then removed it before final release.
enum Bool
{
True,
False,
FileNotFound
};
See https://thedailywtf.com/articles/what_is_truth_0x3f_ enom MsoTrioState {
Toggle,
Mixed,
True,
False,
CTrue
};I also like how True is -1. Beautiful all around!
So I suggested it, got a PR up (bit painful) it got reviewed, went in, everything worked fine, and we came about a week towards releasing the product (6 monthly releases) before someone noticed that we couldn’t load files from previous versions. Turned out that we wrote lots of these old Bool types to binary data files and so the 4 byte data was now being read as 1 byte data. Oops. Reverted the whole lot. Lesson in humility!
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.
All modern C++ collection types have allocators as type parameters, and this was already a thing in the compiler frameworks like OWL during the 90's.
var list = std.ArrayList(i32).init(allocator);
where allocator is a runtime value implementing the allocator interface. fn parseJson(allocator: Allocator, input: []const u8) !JsonValue
Zig forces the caller to decide since allocator is part of the function contract. And this is consistent everywhere in Zig. Effectively speaking, this is universal, explicit & mandated dependency injection for memory management as opposed to to classic C++ STL allocators.Sorry sir, I have a nostalgic fondness for C++ - it was my first language, but it just doesn't compare for flexible allocation convenience compared to Zig.
The irony to argue about manual memory management ergonomic, when code is written by agents.
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.
You're ignoring the fact that it's harder to bring new features to older languages as they have more bloat to deal with as not every idea turns into a success. So younger more focused languages can iterate more quickly. Also, being willing to make breaking changes makes things easier. Microsoft tries hard not to do that with C#.
Over time, maintaining any software becomes harder, languages are no exception. The fact that c# is still around, and still being developed is a feat in itself
VB, C++/CLI and F# are only there because existing customers.
They have always behaved as if it had been a mistake to promote F# from research project into VS 2010 as an official language.
Since then it has been something that the teams never knew how to sell to the .NET customer base, pivoting from being only libraries for C# and VB, write unit tests, Web development, data analysis, whatever might make it.
However it was Standard ML, Miranda, Hope, OCaml and Haskell that lead the way, we aren't still fully there.
Oh, I think we need a citation for these claims.
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.
What matters is what libraries you are gonna use for your solution. If most of them are C#-only and don't have an F# equivalent then you'll lose the ergonomics and conveniences that make F# so easy to work with.
We can all agree that F# is more clever and concise. No one is dying on that hill. But in terms of hacking your way through the customer requirements and working with a team of other humans, it cannot hold ground in the same way.
There is certainly not some concerted effort or lack of care involved. Microsoft could 10x the marketing budget around F# and the adoption rate probably wouldn't budge.
This is just about how you are used to it, it says nothing about the quality of the language itself.
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.
The C# union does not store any discriminator. Just look at the implementation - it's a single `object?` field.
The discriminative part is handled by run-time type information, which is stored in the object itself, not the union - which is why the C# built-in implementation requires boxing.
Also, you can use the same class/record type ("discriminator") in several different unions - again, a feature which ADTs/sum-types/tagged unions in most functional languages do not have.
You can even store one single object (ie. identical by reference equality) in several different union values at the same time, theoretically, which in combination with mutability is... uhh, certainly not common functional/mathematical semantics.
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.
I honestly don't even remotely understand how you got to that from what I wrote.
Unions already have a definition. You cannot go around claiming that technically unions are not unions (and that "most people don't know" this), because theory X or Y overloads the term union to refer to some unrelated concept. That's fine and well and good, within that theory, but it does not displace the usual CS definition of union. It is a load-bearing definition.
A tagged union is so called because it is a union (overlapping memory) with a field (a tag) containing a discriminator (aka discriminated union). An untagged union is a union without this tag. These terms didn't come out of nowhere. They are transparent descriptors of what's happening.
It doesn't matter whether Haskell is implemented in C or Ruby, and it doesn't matter whether it's functional, object-oriented, or a DSL for modding Elden Ring, none of this changes what a union is. I'm very supportive of type theories exploring the option space for type constructs, but someone's going to have to implement these somehow. Such as with (tagged) unions. You know, that CS concept we have, with a settled and well understood meaning.
To be fair to you, I think you're letting a very narrow definition of 'union' shadow a much more usual and common definition of the word. And that's totally fine if that's the definition pertinent to you and your work, but if that's the case, maybe don't go around pontificating about how most people do not understand unions?
I never said this. I said that there are two different kinds of unions with quite different definitions and behaviors.
> A tagged union is so called because it is a union (overlapping memory) with a field (a tag) containing a discriminator (aka discriminated union). An untagged union is a union without this tag.
That alone doesn't sufficiently describe the "unions" in set theoretic type systems like TypeScript. As I said, these unions are not disjoint and don't involve the special constructor that tagged unions/discriminated unions/sum types in algebraic type systems do. They also have logical implications like A implying A|B. Or A|A being equivalent to A, which isn't the case for discriminated/tagged unions. Maybe they are implemented similarly under the hood, but that doesn't negate the difference.
Edit: It's probably fair to say that "untagged unions" in languages like TypeScript, Ceylon or Scala 3 (set theoretic type systems) are different from "untagged unions" in C. It only adds to the confusion...
It's hard to know what to say to this. Tagged unions are not tagged unions, even if they are literal tagged unions? What?
I reiterate what I said in my previous comment, you're not using the ordinary definition of the term union, and this is causing confusion. A union may or may not be a "union" as understood within various academic type theories, that really depends on how any given theory defines that word, which can be any way it wants. But a union is a CS concept with a clearly understood meaning, and when used without added context to suggest it is to be interpreted in some theoretical way, it is understood in that ordinary way.
OP's article is clearly using 'union' to mean tagged unions - he even shows off their implementation, with a tag. The author assumes that his audience will understand what he's talking about when he uses the word union, and it's not causing anyone trouble in this comments section. The fact that alternative definitions within various theoretical paradigms is very nice, bless their hearts, but not really relevant.
You may prefer other definitions to the usual CS definition, that's certainly your prerogative, but - again - that's hardly grounds for taking an article and comment section that's using the commonplace meaning, and appearing to lecture others for failing to adhere to your idiosyncratic standards for what a union must be.
It's neither ambiguous nor confusing to use the word union in CS. The only person who's making it so is you, by introducing semi-unrelated concepts from set theory that happen to have the same name as the established CS concept.
Why stop there? Maybe the author meant the Union, as in the United States? Itself quite ambiguous - does he mean the United States of Mexico, or the United States of the Ionian Islands? Is C# getting Corfu? Corfu dot net? :P
Wrong, see this comment: https://news.ycombinator.com/item?id=48251896
Clearly he thought that it's the same kind of union as in TypeScript and that in C# just the syntax is weird. Which is not the case. Some others who are not commenting are probably also not aware of the two kinds of union types (or three, counting C separately).
> It's neither ambiguous nor confusing to use the word union in CS.
Well, we disagree.
> Well, we disagree.
Most people here know the set theory definition of unions. It's simply a niche use, compared to the usual CS definition, which is the one used in the original article and now all the comments.
You're swimming upstream with a definition that doesn't reflect what is under discussion, which you decreed as though from on high, complete with the assertion that most people don't understand unions like you do. They do.
No this makes no sense at all. Set theoretic types don't use special constructors, there is no way to make them look remotely similar. He is clearly not aware that these are different kinds of unions.
> They do.
Nope, precisely because they are less common. You yourself also clearly also didn't understand the difference between "untagged unions" in C and set theoretic type systems.
You have come into a room full of CS practitioners to announce to them that you alone understand what unions are. Never mind fifty years of industry practices and nomenclature - never mind the fact we all already know set theory and unlike you don't confuse set theoretic unions with tagged unions - all that can now be set aside because you discovered set theory last week and now no one understands unions except for you.
Can't wait for next week when you discover some new band, and you'll be in here telling us how no one gets music except for you. :P
Boxing is not something inherently to be avoided. It actually can work better in many (most?) use cases, and avoids a lot of problems that non-boxing approaches often cause (like tearing and copy costs).
It's try that the non boxing pattern could be implemented by us. And it's very reasonable that that is something we may do post this release. However, it's a non-trivial area. There's no one correct 'non-boxed' implementation. For example, do you have separate fields for all your unmanaged data? or do you have a blob of bytes that is large enough to align all your unmanaged data from teh largest set of of unmanaged fields, and you unsafe index into that?
Similar question for managed data. Do you have strongly typed fields for that data? Or do you attempt to use objects, to compact to as little space as possible? The former avoids casting costs. The latter allows you to minimize space. You can also potentially use unsafe casts. But those might introduce memory holes in tearing situations. etc. etc.
Because of this, i think the best outcome is to define the pattern (which we've done) and then use generators to allow you to control precisely the impl strategy, giving you all the bells and knobs you want to best fit your domain.
They still feel like something only .NET team masters, and a few MVPs that wrote key reference blog posts about them.
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
Contrary to what a lot of people guess, boxing is actually a really good strategy most of the time. And, is indeed what many people are doing here anyways. The design supports a pattern that allows for non-boxing, and I expect that we will both supply an implementation for that with reasonable defaults, and that source generators will be a great way to augment this to get highly specialized impl strategies for non-boxing depending on the varying domain needs any specialized customer may have.
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.
public record Left<T>(T Value);
public record Right<T>(T Value);
public union Either<L, R>(Left<L>, Right<R>);You're correct. The unions we're working on right now are 'type unions'. So the type is inherent in the union distinction, and you would not be able to distinguish that case. That said, we're also looking at full blown discriminated unions (you can look at one of my proposals for that here: https://github.com/dotnet/csharplang/blob/main/meetings/work...), which would allow for that. Syntax entirely tbd, but you'd do something like:
enum struct Either<T1, T2> // or enum class
{
First(T1 value),
Second(T2 value)
}
We view these features as complimentary. Indeed, if you look at the extended enum proposal, you'll see it builds on top of unions and closed types (another proposal coming in the next version of the lang).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...
# Python
MyStringBool = Literal("Yes") | Literal("No")
// TypeScript
type MyStringBool = "Yes" | "No"
I assume it exists to compensate for the previous lack of typing, and consequent likelihood of ersatz typing via strings.It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:
data MyBool = Yes | No
Of course you'd need a trivial parser, though this is probably a good idea for any string type: parseMyBool :: String -> MyBool
parseMyBool "Yes" = Yes
parseMyBool "No" = No
parseMyBool _ = error "..."
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example: @type my_bool() :: :yes | :no
@spec parse_my_bool(String.t()) :: my_bool()
def parse_my_bool("Yes"), do: :yes
def parse_my_bool("No"), do: :no
def parse_my_bool(_), do: throw("...")
Where :yes and :no are memory-efficient symbols, not strings.In TS in particular, in combination with other features (mapped types), they are equivalent to row polymorphism + whatever Haskell/GHC features enable type families to specialize on constant literal arguments (or you can use atomic types, but that's not structural / open-world)... so pretty advanced.
This is valid TS/Python:
type ABC = "A" |"B" | "C"
type AB = "A" | "B"
const x: AB = "A";
const y: ABC = x;
The equivalent Haskell requires using several extensions.My overall point is that Haskell's type system is sufficiently expressive (you may not have "A" | "B" | "C", but you do have A | B | C) that there's no obvious remaining use case for string literals, unless you're thinking of typing input by way of expected literals instead of actually parsing it, which is... a choice. :P
Because TypeScript has structural sub-typing, while standard Haskell (eg. `A | B | C`) has neither subtyping nor structural typing, which both are very useful features for safe "integration/glue" type of programs.
(String) literals form a fundamental part of the TS "row polymorphism" (record types) and eg. tuple union type implementation.
You can type a non-empty array that starts with zero...
type Arr = [0, ...number[]];
const a: Arr = [0, 1, 2, 3, 4]
Now try in Haskell.No? What extensions does `A | B | C` require?
> Haskell has neither subtyping nor structural typing
Is subtyping back in? Good news for Java and C++.
Re structural typing, I would ask what behaviour you're after, specifically. For example, this is a valid, typed Haskell function for any two values that can be added, including any user-defined ones:
adder a b = a + b
If by structural typing you mean silently coercing types that the compiler deems structurally equivalent, then no, but I don't think many people writing Haskell would consider that desirable. A `Person` may have an age (40) and a `Wine` may have an age (2005), but you're not going to get sensible results if you start adding those two together, and your compiler should probably stop you.Structural typing is the sort of thing that is very valuable if you're bolting a type system onto a language with a cornucopia of untyped structs, like JS objects. It is comparatively much less valuable if you're working in a typed ecosystem to begin with, since you're not liable to have loose untyped structs floating around that require coercion.
> "integration/glue" type of programs
It does sound a lot like you're using string literals in lieu of parsing foreign input, which strikes me as a pretty bad idea. Particularly in a language like TS, which is not type safe at runtime, and which will happily ingest an unexpected value, silently coerce it in all sorts of fun and wacky ways, and cause behaviour far removed from what any static analysis of the TS source would suggest.
> You can type a non-empty array that starts with zero
Can you please name me any possible actual use for this? Especially given the type doesn't even exist at runtime and will never be enforced on input data, so this is a once-off check for comptime constants?
The Person/Wine example is a pointless strawman. That's not what structural typing is generally used for.
The entire comment is basically making up strawmans... I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.
TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.
To conclude, these type system features were added by multiple experienced language designers for a reason, to languages that already had functional ADTs, so going "huh but what are these even useful for?!" to me sounds a bit clueless (or argumentative), so I don't see a productive continuation to this discussion.
> these type system features were added by multiple experienced language designers for a reason
Oooh, an appeal to authority, where that authority isn't even named. I'll have you know a famous queen told me you're wrong on this, also for "a reason".
> TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.
Right, so TS typing is so amazing it requires runtime parser libraries from NPM, and Haskell is less sophisticated because it's not stringly typed.
You realise the entire, complete, exhaustive runtime schema for your zero-first non-empty integer array example looks like this in Haskell, right?
sch (0:_) = True
sch _ = False
That's a complete function that somehow manages to work without pulling in NPM dependencies. The best JavaScript minds of our generation remain baffled.> The Person/Wine example is a pointless strawman
> I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.
So when you use illustrative examples, it is to "disambiguate what you meant" (huh?), and when I do it, they're "pointless strawmen". A little hypocritical, no?
> a bit ignorant
Honestly, I don't think you know what you're talking about at all. You clearly hadn't even read my comment when you started replying with Python and TS examples... that were already in my comment.
It also really sounds like you're using string literals to type input without properly parsing it, which is just a terrible idea. Haskell's type system is designed precisely to protect you from this sort of mistake. [0] No, you're not always going to get what you expect. No, your JS program will never let you know that's the case. No, a sane type system does not require mainlining runtime parser libraries from the biohazardous oceans of NPM. A schema in Haskell is going to be significantly shorter and sounder than anything in Zod, and you don't need a library for it.
As I said above, TS' type system makes sense for a type system bolted onto a dynamic language post facto. TS needs to more tightly link (even mildly conflate) values and types, since it needs to do a lot of clever narrowing to figure out what mad ball of JS it is dealing with at any given time. Haskell does not operate under any such constraint.
> I don't see a productive continuation to this discussion.
Phew. Timesaver.
Of course the irony of all this is that I use TS daily, and Haskell quite rarely.
[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
Does not seem like this:
> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.
> Personally, I consider all of Go to be a mistake.
You also consider Scala and OCaml to be mistakes? Because all of what I've mentioned also works in a very similar way in Scala.
> an appeal to authority
No, I didn't appeal to authority. It's the opposite - it's statistics. Multiple experienced language designers don't add features later for nothing. For a similar process, see eg. closures getting added to every mainstream language.
> complete, exhaustive runtime schema
Again, I wasn't talking about runtime schemas, but types. I only mentioned runtime as a counterpoint to the false statement that TypeScript doesn't enforce this. Only reducing this to runtime checking is a fallacy, again.
> A little hypocritical, no?
No, they aren't comparable since I wasn't using the examples as supports for an argument of whether X is better than Y. Strawmen involve argumentation.
> read my comment... examples... that were already in my comment
I read it in detail, the problem is you didn't really read my comment in detail, which illustrated both the subtyping and structural typing aspects (albeit trivially, yes), which yours didn't.
> You're also fairly clearly using string literals to type input without properly parsing it
Okay then, the arrogance of this is pretty astounding... You seem to know what I'm doing better than I am!
To be clear, I'm not doing any of that. And I've written Haskell way before I wrote any TypeScript.
Everything I said to you prior to my last message was fairly gentle. I'm not sure what response you were expecting to what you wrote at that point, which was to accuse me of disingenuous strawman arguments and ignorance. Perhaps you yourself would have benefited from a rule refresher?
> You seem to know what I'm doing better than I am
Apparently so! Trust me, it gives me no pleasure, and I'd rather I didn't.
> Again, I wasn't talking about runtime schemas, but types. I only mentioned runtime as a counterpoint to the false statement that TypeScript doesn't enforce this. Only reducing this to runtime checking is a fallacy, again.
My dear friend, this is almost completely incoherent.
I wish to strictly type myself as a function at this point, whereby your messages are my input, and void is my output. Zod, activate!
I began by noting that TS has literals and set theoretic types, and that this makes sense for a post-facto type system bolted on top of a dynamic language. Jaen showed up to inform me that TS has literals and set theoretic types, implying he hadn't read my post.
I noted that typed string literals are not generally considered desirable within strictly typed environments. "Parse, don't validate", etc. Jaen seemed to follow this up by aggressively trying to prove that TS is somehow "better" than Haskell. His arguments comprised the fact that TS has literals and set theoretic types (again, yes, this was is in my initial post), and a mixture of personal insults and just straight up nonsense (I wasn't the one to bring Zod into a discussion of type systems... ). At that point I did have a little fun with things, since it had become clear Jaen was not a constructive or good faith interlocutor.
Jaen's central misapprehensions seem to be that (a) I don't understand literals and set theoretic types, despite this whole thread being in reply to a post where I give examples of them in TS; and (b) that I care which type system is "better".
As I repeated a number of times above, TS' type system makes sense for a type system bolted onto a dynamic language. It's extremely useful when the underlying language has oodles of untyped structs flying every which way. Conversely, Haskell's type system makes sense within a holistic strictly typed environment. Structural typing would be a gaping hole in Haskell's strict type safety, which is kind of Haskell's whole thing. Neither system is better, each has its use. Different strokes for different folks.
I don't usually put much stock in upvotes, but I do note I seem to have the edge there. Seems that our esteemed panel of armchair referees respectfully dissent from your narrative, nvlled. :)
> I began by noting that TS has literals and set theoretic types
Can you please quote me exactly the part of your original comment that implies that literal types are structural and have subtyping? Because that is what I said. If you did imply that, well, excuse me for helping you by clarifying then.
> aggressively trying to prove that TS is somehow "better" than Haskell
Sorry, what? Refer to the first line of this comment. Just saying that some type system features are extensions or harder to use in Haskell says nothing about the superiority of TS. This is completely your imagination.
Here's me criticizing TS 4 months ago: https://news.ycombinator.com/item?id=46501061
> I wasn't the one to bring Zod into a discussion of type systems...
Not sure why this is even relevant, but this was a direct reply to: "...language like TS, which is not type safe at runtime, and which will happily ingest an unexpected value, silently coerce it in all sorts of fun and wacky ways"
Zod (just a random library) is a direct counter-example to that. There may be better counter-examples. One just needs a proof of existence - it doesn't have to be good.
I then had to repeat that I am also talking about static types because of: "...exhaustive runtime schema for your zero-first non-empty integer array example...". TS can both enforce that as a type (which you never presented for Haskell) and as a value. (nominally typed solutions quickly run into ergonomic problems like phantom types not being composable across library boundaries - eg. refinement types, which are even safer by nature, are structural!)
In any case, I'd estimate 99% of people using TS don't encounter any type safety issues caused by the design of TS. Haskell has `unsafeCoerce` too, just a bit wordier than in TS.
If wanting to talk about real unsoundness, one would mention something like bivariance (see also: linked comment), but even then almost all of that is entirely irrelevant in most practical software engineering.
> (a) I don't understand literals and set theoretic types, despite this whole thread being in reply to a post where I give examples of them in TS; and (b) that I care which type system is "better".
Huh, what? Again, I'm thinking none of that. Again, you imply you know better what I'm thinking...
> Structural typing would be a gaping hole in Haskell's strict type safety
I mean, yeah, let me just repeat OCaml and Scala here, both also famously type safe languages... Why make an argument when there's two immediate counterexamples that were already mentioned?
> but I do note I seem to have the edge there
This pretty much sums up the difference in attitude, I'm not here to score internet points in an argument.
I just wanted to comment on why the world is not black and white and even technically flawed languages like TS have something to learn from.
That Haskell snippet is just syntax sugar for Left(string) | Right(string), which is trivial in any language with unions.
Not clear why it would be an improvement over just naming the alternatives something meaningful, but if you're wedded to Left and Right, go for it.
The C# unions appear to behave like unions, not disjoint unions.
You are correct that this requires support for disjoint unions (aka tagged unions), which Haskell always had and C# will soon have.
type T1 string
type T2 string
type Meh Either<T1, T2>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.
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.
A nice balance of language features and legibility.
Come on Gophers! It's time.
Note that my expectation would be that the non-boxed form would be as trivial as adding `[NonBoxedUnion(SomeImplStrategyChoiceEnum)]` (or `[NonBoxedUnion]` for some default strategy choices that likely are ok).
This would give you extremely fine grained flexible choice on how you wanted your non-boxing union to work. There's no single right answer. There are just tradeoffs in terms of space/speed/copying-costs/memory-safety/etc.
I think it would make the most sense as people who care about boxing will have very different views and needs in terms of things like space, casting costs, copying speed etc.
The vast vast majority of users do not need to care at all. And for that, a boxed approach works exceptionally well.
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.
My primary concern with this pattern versus exceptions is calling code can simply discard the resulting problem.
Except with a Result type the fact that an exception can occur and should be handled in the first place is explicit.
The problem if anything is that you MUST say something about the error case, despite the common scenario being “pass it forward” — the same reason exception do this by default. Which is also why rust for example special cases Result with the ? operator to do exactly that
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.
type Expr =
| Primitive of int
| Addition of (Expr * Expr)
| Subtraction of (Expr * Expr)
| Negation of Expr"Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has 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.
"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.
As a 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 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.
Good languages should support both!
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.
------------------------------------------------
Some real code (but it's F# not C#) to look at.
AST for my SQL dialect: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Typechecker code: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Backend code that outputs MS TSQL from it: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez...
------------------------------------------------
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.
------------------------------------------------
It doesn't just have to be compiler stuff. A business app data model can use this too. Instead of having:
public class DbUser
{
public EmailAddress Email { get; set; }
public PasswordHash? Password { get; set; } // null if they use SSO
public SamlEntityProviderId? SamlProvider { get; set; } // null if they use password auth
}
You could have: type UserAuth =
| PasswordAuth of PasswordHash
| SSOAuth of SamlIdentityProviderId
The implementation details of those different auth methods, the UI for them, etc. don't have to be part of the data model. We do have to model what "shapes" of data are acceptable, but "doing stuff" based on those shapes is another layer's problem.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.
can't my co-worker just use this pattern and discard an error result the same? I'd argue its easier as the stack wont unwind by default because the error is returned instead of thrown.
Yeah, I'm mildly sold on this use-case to be fair. But I think I'll keep the unexpected errors as exceptions.
Result<T,E> fn()
T | E fn()
T fn() throws E
fn() throws(E) -> TThe 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 switched between dozens of similar codebases over a period of 3-4 years (pre AI) when I was consulting and did multiple projects in multiple languages (well, only 1 in rust).
In my experience switching between the C# projects was always the worst. The codebase semantics diverged in ways I simply didn’t see in the Java/C++ codebases.
Now this one sounds almost unbelievable :). I’ve yet to see 3 or more C++ codebases using the same language feature set.
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...
Compare this to exceptions, where the type is just `T` and can be used without further ceremony. You can discard the error by forgetting a handler. Now you have a program that occasionally crashes.
Follow-up: Are there async exceptions? A `Result` is just data that can be awaited. How would that work with exceptions?
It’s a very decent language (I mean C#) and runtime, I wish it had more market share in the startup world.
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!
Rider is definitely the most equivalent to full Visual Studio though.
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.
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)
companies spend a lot on marketing, and it's not just ads.