enum Bool
{
True,
False,
FileNotFound
};
See https://thedailywtf.com/articles/what_is_truth_0x3f_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!
enom MsoTrioState {
Toggle,
Mixed,
True,
False,
CTrue
};I also like how True is -1. Beautiful all around!
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.
Turns out they really want to have plenty of Java development on Azure as well.
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.
Are you able to elaborate why? Just curious.
I love C# the language, but the ecosystem is a ghetto.
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
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.
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.
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.
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.
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.
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.The C# unions appear to behave like unions, not disjoint unions.
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?
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?
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.
Come on Gophers! It's time.
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.
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.
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."Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has them.
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!
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.
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.