• Hacker News
  • new|
  • comments|
  • show|
  • ask|
  • jobs|
  • utf_8x 12 hours

    Hell yeah! After all these years it's finally here.

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

    It would be great to be able to do something like

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

  • enbugger 12 minutes

    Cool, you now can implement Elm architecture inspired GUI framework in C#.

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

  • kkukshtel 20 hours

    [flagged]

  • merb 15 hours

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

  • mirages 21 hours

    #define struct union

  • DeathArrow 19 hours

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

    IcyWindows 19 hours

    The article mentions that one can implement a union type that doesn't do boxing.

  • DeathArrow 20 hours

    This is HUGE! Now we can use mostly functional programming in C#. This feature was requested since many years ago.

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

    littlecranky67 16 hours

    Minimal API is pretty functional.

  • jcmontx 20 hours

    So they finally took all of the cool features from F#. What's missing? The pipe operator for railway oriented programming?

    algorithmsRcool 20 hours

    Well off the top of my head...

    Active patterns, computation expressions, structural typing, statically resolved type parameters, explicit inlining, function composition, structural equality, custom operators and much richer generators.

    JMKH42 20 hours

    type providers, units of measure, active patterns, complete type inference.

    Not sure I would want the last thing in C#, I think having boundaries at the function signature for that.

    pjmlp 2 hours

    You can fake type providers with code generators though.

    recursive 20 hours

    Units of measure

    owlstuffing 20 hours

    F# units are handy, but nothing like Manifold units (Java):

    https://github.com/manifold-systems/manifold/tree/master/man...

  • SuperV1234 2 hours

    Boxed. Killed all my excitement.

    Turskarama 2 hours

    You should have kept reading.

      For performance-sensitive scenarios where case types include value types, libraries can also implement the non-boxing access pattern by adding a HasValue property and TryGetValue methods. This lets the compiler implement pattern matching without boxing.

    wiseowise 1 hours

    Still a manual step, pass.

  • amluto 16 hours

    Hmm, they seem to have chosen to avoid names to the choices in the union, joining C++ variants and (sort of) TypeScript unions: unions are effectively just defined by a collection of types.

    Other languages have unions with named choices, where each name selects a type and the types are not necessarily all different. Rust, Haskell, Lean4, and even plain C unions are in this category (although plain C unions are not discriminated at all, so they’re not nearly as convenient).

    I personally much prefer the latter design.

    dgb23 1 hours

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

    - enum: essentially just a typed uint tag collection

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

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

    Read from this section and they appear in order:

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

    ks2048 11 hours

    I would love to see a page that has cross-language comparisons of how different structures work. Eg “unions: differences between langs. Enums: …” while grouping together the different design choices, as you do in this one case.

    I suppose an LLM could do a pretty good job at this.

    gf000 4 hours

    Careful not to mix unions with sum types, though. The key distinction is that the latter are disjunct sets, even if you "sum" together the same type twice, you can always tell which "way" you went.

    An example that may show the difference: if you have a language with nullable types, then you basically have a language with union types like String|Null, where the Null type has a single value called `null` and String can not be null.

    Now if you pass this around a function that itself may return `null`, then your type coalesces to String|Null still (you still get a nullable string, there is no doubly nullable). This is not true for Maybe/Option whatever you call types, where Some(None) (or Optional.of(Optional.empty())) is different from None only.

    Rich Hickey once made a case that sort of became controversial in some FP circles, that the former can sometimes be preferred (e.g. at public API surfaces), as in for a parameter you take a non-nullable String but for returns you return a String|Null. In this case you can have an API-compatible change widening the input parameters' type, or restricting the return type - meanwhile with sum types you would have to do a refactor because Maybe String is not API compatible with String.

    Zecc 1 hours

    > Careful not to mix unions with sum types, though. The key distinction is that the latter are disjunct sets, even if you "sum" together the same type twice, you can always tell which "way" you went.

    This is a really good point. I'd love to be able to have a sum type of two strings ("escaped" and "unescaped"); or any two kinds of the same type really, to model two kinds of the same type where one has already passed some sort of validation and the other one hasn't.

    Edit to add: I figure what I want is for enums to be extended such that different branches are able to carry different properties.

    Edit again (I should learn to think things through before posting. sorry): I suppose it can be faked using a union of different wrapper types, and in fact it might be the best way to do it so then methods can take just one of the types in arguments and maybe even provide different overloads.

    repelsteeltje 15 hours

    Not sure, but I think C++ actually does allow std::variant with multiple choices using the same type. You might not be able to distinguish between them by type (using get<Type>()), but you can by position (get<0>(), get<1>(), ...)

    NooneAtAll3 15 hours

    I think GP is talking about name-of-field access, not index access or name-of-type

    amluto 14 hours

    I haven’t tried this, and I don’t intend to, because visitors and similar won’t work (how could they?) and I don’t want to have to think about which is choice 2 and which is choice 7.

    Metasyntactic 14 hours

    Hi there! One of the C# language designers here, working on unions. We're interesting in both forms! We decided to go with this first as felt there was the most value here, and we could build the named form on top of this. In no way are we thinking the feature is done in C#15. But it's part of our ongoing evolution.

    If you're interested, i can point you to specs i'm writing that address the area you care about :)

    Shalomboy 13 hours

    Not OP but I would love to check that out

    Metasyntactic 13 hours

    Sure, as an example: https://github.com/dotnet/csharplang/blob/main/meetings/work...

    Again, very rough. We go round and round on things. Loving to decompose, debate and determine how we want to tackle all these large and interesting areas :)

    moi2388 1 hours

    Oh I’m very happy to hear this is being worked on!

    amluto 10 hours

    Nice! (Well, nice the first time I loaded the page, but GitHub appears to be rocking maybe 90% uptime today and I can’t see it anymore.)

    I admit that I haven’t actually used C# in 20 years or so.

    tialaramex 16 hours

    The C and Rust union types are extremely sharp blades, enough so that I expect the average Rust beginner doesn't even know Rust has unions (and I assume you were thinking of Rust's enum not union)

    I've seen exactly one Rust type which is actually a union, and it's a pretty good justification for the existence of this feature, but one isn't really enough. That type is MaybeUninit<T> which is a union of a T and the empty tuple. Very, very, clever and valuable, but I didn't run into any similarly good uses outside that.

    amluto 14 hours

    > The C and Rust union types are extremely sharp blades

    Sure, but the comparable Rust feature is enum, not union.

    SkiFire13 4 hours

    FYI small string optimizations are generally implemented using unions.

    tialaramex 2 hours

    In Rust? The two I'm big fans of, CompactString and ColdString do not use unions although historically CompactString did so and it still has a dependency on smallvec's union feature

    ColdString is easier to explain, the whole trick here is the "Maybe this isn't a pointer?" trick, ColdString might be a single raw pointer onto your heap with the rest of the data structure at the far end of the pointer, this case is expensive because nothing about the text lives inline, but... the other case is that your entire text was hidden in the pointer, on modern hardware that's 8 bytes of text, at no overhead, awesome.

    CompactString is more like a drop-in replacement, it's much bigger, the same size as String, so 24 bytes on modern hardware, but that's all SSO, so text like "This will all fit nicely" fits inline, yet the out-of-line case has the usual affordances such as capacity and length in the data structure. This isn't doing the "Maybe this isn't a pointer?" trick but is instead relying on knowing that the last byte of a UTF-8 string can't have certain values by definition.

    pjmlp 2 hours

    In Rust's case, union types should only be used for FFI with a C ABI.

    As for C, it is a sharp blade on its own.

    tialaramex 1 hours

    I do not agree. MaybeUninit<T> is without any doubt more valuable than the C FFI use

    I can't even think of any prominent C FFI problems where I'd reach for the union's C representation. Too many languages can't handle that so it seems less useful at an FFI edge.

    masklinn 16 hours

    Unions can be used as a somewhat safer (not safe by any means but safer), more flexible, and less error-prone form of transmute. Notably you can use unions to transmute between a large type and a smaller type.

    That is essentially the motivation, primarily in the context of FFI where matching C's union behaviour using transmute is tricky and error-prone.

    randomNumber7 15 hours

    There are rare cases where all attributes of the C union are valid at the same time. Say you have a 32-bit RGBA color value and you want to access the individual 8 bit values. You can make a union of an 32 bit int and a struct that contains 4x 8 bit integers.

    Also you can manually tag them and get s.th. more like other high level languages. It will just look ugly.

    Animats 12 hours

    Yes. I once wanted C unions limited to fully mapped type conversions, where any bit pattern in either type is a valid bit pattern in the other. Then you can map two "char" to "int". Even "float". But pointer types must match exactly.

    If you want disjoint types, something like Pascal's discriminated variants or Rust's enums is the way to go. It's embarrassing that C never had this.

    Many bad design decision in C come from the fact that originally, separate compilation was really dumb, so the compiler would fit in small machines.

  • tialaramex 19 hours

    I don't love OneOrMore<T>

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

    layer8 15 hours

    > so our type is really "OneOrMoreOrNone"

    If I understand correctly, it’s actually OneOrOneOrMoreOrNone. Because you have two different distinguishable representations of “one”.

    The only reason to use this would be if you typically have exactly one, and you want to avoid the overhead of an enumeration in that typical case. In other words, AnyNumberButOftenJustOne<T>.

    rafaelmn 17 hours

    > OneOrMoreOrNone

    So IEnumerable<T> ? What's up with wrapping everything into fancy types just to arrive at the exact same place.

    paulddraper 16 hours

    I went to prove you wrong…

    And you’re exactly right.

    It’s not “one or more.”

    It’s “one or not one.”

    Need two or not two.

    recursive 16 hours

    Hotdog or not hotdog.

    CharlieDigital 18 hours

    `OneOrMore<T>` was an example of using `union` types.

    You are free to call it `public union Some<T>(T, IEnumerable<T>)`

    gf000 4 hours

    But now you can only call methods that are available for both T and IEnumerable<T>, you have no way of knowing which it actually is. (You would know if it were sum types)

    merb 17 hours

    OneOrMore is more or less an example from the functional world. i.e.:

    https://hackage.haskell.org/package/oneormore or scala: https://typelevel.org/cats/datatypes/nel.html

    it's for type purists, because sometimes you want the first element of the list but if you do that you will get T? which is stupid if you know that the list always holds an element, because now you need to have an unnecessary assertion to "fix" the type.

    dasyatidprime 16 hours

    The NonEmptyList in Cats is a product (struct/tuple) type, though; I assume the Haskell version is the same. The type shown in the blog post is a sum (union) type which can contain an empty enumerable, which contradicts the name OneOrMore. The use described for the type in the post (basically a convenience conversion funnel) is different and makes sense in its own right (though it feels like kind of a weak use case). I'm not sure what a good name would've been illustratively that would've been both accurate and not distracting, though.

    merb 15 hours

    Well you are right of course, I just wanted to explain what they wanted to show. Of course the type would be wrong if the second entry in itself is an empty list. I just wanted to explain the reasoning what they tried to accomplish

    They could’ve done the Either type which would’ve been more correct or maybe EitherT (if the latter is even possible)

    dasyatidprime 15 hours

    I don't think they were trying to accomplish the same thing as the Scala/Haskell version; these are just two completely different things that happen to share a name because the blog post gave the example a name that is confusing when read literally. The purpose of the Cats version is “there is always a head element”. The purpose of the union in the blog post is more like “this can be a collection, but many callers will be thinking of it as a single element, so don't put the burden on them to convert it”. I do think it's a weak case for them in a type theory sense (I would tend to position that kind of implicit conversion elsewhere in the language), but I can also see it being motivating to a large class of developers…

    … wait, I've made a different mistake here while trying to explain the difference, haven't I? I was describing it as a sum type, but it's not really a sum type, it's really just set-theoretic union, right?

    Which also means OneOrMore is unsound in a different way because it doesn't guarantee that T and IEnumerable<T> are disjoint; OneOrMore<object> initialized from [x] will always return [[x]] from AsEnumerable, won't it? If I'm interpreting the switch expression correctly and the first case predominates, since a list is-an object? I don't have a test setup handy; someone with actual C# experience, please tell me whether that's correct or whether the compiler signals an error here or something…

  • mwkaufma 20 hours

    Looks like it's "just" type-erasure / syntactical sugar. E.g. value types are boxed.

    functional_dev 18 hours

    Right, the default boxes into heap, but unions are different. Some languages pack them as a flat struct (tag + payload, no allocation).

    Here is visual layout if anyone is interested - https://vectree.io/c/memory-layout-tagging-and-payload-overl...

    debugnik 13 hours

    That is not what C# has just added to the language though. These union types so far are just wrappers over an `object` field which gets downcasted.

    F# offers actual field sharing for value-type (struct) unions by explicitly sharing field names across cases, which is as far as you can push it on the CLR without extra runtime support.

    AndrewDucker 20 hours

    Yes, but see the section on custom unions* - you can write non-boxing unions/generators.

    * https://devblogs.microsoft.com/dotnet/csharp-15-union-types/...

    mwkaufma 18 hours

    Yes, there's a compat-shim in the stdlib/runtime, but not in the language syntax. E.g. it by-definition won't do escape-analysis and optimize discriminated value-types with the first-class keyword.

    Izikiel43 14 hours

    For now

    celeries 20 hours

    Yes, but that's just the default behavior. You can implement your own non-boxing version for performance critical applications.

    algorithmsRcool 17 hours

    Why on earth did they decide boxing by default was a sensible design decision...

    We have been pushing toward higher performance for years and this is a performance pitfall for unions would are often thought of as being lighter weight than inheritance hierarchies.

    F# just stores a field-per-case, with the optimization that cases with the same type are unified which is still type safe.

    Metasyntactic 14 hours

    Hi there! One of the C# language designers here, working on unions. All the different options have tradeoffs. As an example, the non-boxing options tear, which can be problematic. And, we have a lot of experience implementing the simple, reference-type, approach for types that make a lot of sense to people, but then adding a lightweight, value-type version for people who care about that later. See tuples, as well as records.

    I expect the same will old here. But given the former group is multiple orders of magnitude higher than the latter, we tend to design the language in that order accordingly.

    Trust me, we're very intersted in the low-overhead space as well. But it will be for more advanced users that can accept the tradeoffs involved.

    And, in the meantime, we're designing it in C#15 that you can always roll the perfect implementation for your use case, and still be thought of as a union from teh language.

    debugnik 13 hours

    > with the optimization that cases with the same type are unified which is still type safe.

    To be clear, this requires explicitly using the same field name as well.

    zigzag312 15 hours

    From what I've read, this is for the first implementation of unions, to reduce amount of compiler work they need to do. They have designed them in a way they can implement enhancements like this in the future. Things like non-boxing unions and tagged unions / enhanced enums are still being considered, just not for this version.

    CharlieDigital 15 hours

    This is the general pattern of how the C# team operates, IME.

        "Never let perfect be the enemy of good"
    
    Very much what I've seen from them over the years as they iterate and improve features and propagate it through the platform. AOT as an example; they ship the feature first and then incrementally move first party packages over to support it. Runtime `async` is another example.

    pjmlp 2 hours

    In the meantime I still haven't done any project with nullable references, because the ecosystem has yet to move along. Same applies to ValueTask for async code.

  • 98347598 22 hours

    It's very disappointing that they aren't supporting Rust-style discriminated unions.

    pjmlp 2 hours

    ML-style discriminated unions, actually.

    Metasyntactic 14 hours

    Hi there! One of the C# language designers here, working on unions. We're extremely interested in discriminated unions. A real problem is that there so much interest, with many varying proposals on how best to do them. It's a lot to go through, and we've found some of the best designs layer on standard unions. So we like this ordering to lay the foundation for discriminated unions to built on top of! :)

    hnthrow0287345 22 hours

    One step at a time

    adrian_b 13 hours

    These are discriminated unions, even if they may be not Rust-style.

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

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

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

    gf000 3 hours

    I don't think these are discriminated. From the docs:

    > Union types — exhaustive matching over a closed set of types

    > Closed hierarchies — exhaustive matching over a sealed class hierarchy

    > Closed enums — exhaustive matching over a fixed set of enum values

    I believe the last one would be sum types (disc. unions). This one allows overlapping types.

    adrian_b 2 hours

    The word "discriminated" means that at run time, if you receive a value whose type is a union of types you can discriminate which is the actual type of the value, so you can use that value in expressions that expect a specific type, not a union of types.

    This is in contrast with what is called "union" in the C language, where you must know a priori the type of a value in order to use it correctly.

    Moreover, if you can discriminate at run time which is the actual type, that means that you can discriminate between values that happen to have the same representation, but which come from distinct types, e.g. if you had a union between "signed integer" and "unsigned integer", you could discriminate between a signed "3" and an unsigned "3".

    This property ensures that the number of possible values for a discriminated union type is the sum of the numbers of possible values for the component types, hence the alternative name "sum type", unlike for a non-discriminated union, where the number of possible values is smaller, because from the sum you must subtract the number of possible values of the intersection sets.

    The C# unions described in the article are discriminated unions, except that their component types are not the types literally listed in their definition. If some of the component types are optional, than the true union components are the corresponding non-optional types, together with the null a.k.a. void type, which I find as a rather strange choice.

    _old_dude_ 21 hours

    In C#, all instances have a class, so there is already a discriminant, the class itself.

    In the article, the example with the switch works because it switches on the class of the instance.

    cobbal 16 hours

    Null doesn't. `union(Int?, String?)` will only have 1 type of null, unlike a proper discriminated union.

    adrian_b 13 hours

    The C# unions as described are discriminated unions.

    The fact that they flatten a union of optional types into an optional union of the corresponding non-optional types is indeed a weird feature, which I do not like, because I think that a union must preserve the structural hierarchy of the united types, e.g. a union of unions must be different from a union of all types included in the component unions, and the same for a union of optional types, where an optional type is equivalent with a union between the void/null type and the non-optional type, but this C# behavior still does not make the C# unions anything else but discriminated unions, even if with a peculiar feature.

    gf000 3 hours

    > that a union must preserve the structural hierarchy of the united types, e.g. a union of unions must be different from a union of all types included in the component unions, and the same for a union of optional types, where an optional type is equivalent with a union between the void/null type and the non-optional type

    This is exactly the difference between simple union types and discriminated unions. This c# feature is what typescript has, not what Haskell/java/f#, etc.

    adrian_b 2 hours

    The word "discriminated" by itself does not specify this property.

    "Discriminated" just means that at run time you can discriminate the values of a union type by their current type, so you can use them correctly in expressions that expect one of the component types.

    I agree that the right implementation of discriminated types is that mentioned by you and which is that of many important programming languages, but even if I disapprove of this property that the C# unions have, which in my opinion may lead to unexpected behavior that can cause subtle bugs, the C# unions are still discriminated unions, where you can discriminate the current type at run-time, with a "switch".

    In my opinion, one should avoid this weird behavior of C#, by always defining only unions of non-optional types. Where needed, one should then define an optional type having as base a union type. Then these unions will behave like the discriminated unions of other languages.

    Whether you use or not this policy, there are types that the C# unions cannot express, but if you use this policy, at least the limitations become explicit.

  • karmakaze 3 days

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

    let_rec 22 hours

    > ad-hoc unions (on the fly from existing types) that are possible in F#

    Are you sure? This is a feature of OCaml but not F# IIUIR

    Edit: https://github.com/fsharp/fslang-suggestions/issues/538

    dathinab 21 hours

    it's basically `union <name>([<type>],*)`, i.e.

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

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

    CharlieDigital 21 hours

    IME, this is a good thing.

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

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

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

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

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

    mikeocool 13 hours

    Yeah, Typescript feels like it had has arrived at the point where someone needs to write “Typescript: the good parts” and explains all of the parts of the language you probably shouldn’t be using.

    owlstuffing 21 hours

    > It doesn't cover ad-hoc unions

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

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

        Bar | Baz
    
    It is the same as the night and day difference between tuples and nominal records.

    Metasyntactic 13 hours

    Hi there! One of the C# language designers here, working on unions.

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

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

    wsve 4 hours

    Very good to hear that!

    pjmlp 2 hours

    Well, there is also the issue that some things get designed and then abandoned even thought some improvements were expected, dynamic typing from DLR, expression trees, for example.

    mpawelski 19 hours

    I'm pretty sure at one point there was proposal that allowed declaring something like `int or string`. Not sure what happened with it though.

    orthoxerox 22 hours

    Third paragraph from the top:

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

    SideburnsOfDoom 22 hours

    It's very unclear which you mean by that.

    To me that "compiler-verified" maps to "sealed", not "on the fly". Probably.

    Their example is:

    public union Pet(Cat, Dog, Bird);

    Pet pet = new Cat("Whiskers");

    - the union type is declared upfront, as is usually the case in c#. And the types that it contains are a fixed set in that declaration. Meaning "sealed" ?

    gf000 4 hours

    I think your "sealed" is misleading here, as that is used for sum types in similar languages (java).

    As the language designer notes in the comments, these are named unions, as opposed to anonymous ones, but they are also working on the latter.

    "Sealed" is probably not the correct word to use here, as it would be sealed in both case (it doesn't really make sense to "add" a type to the A | B union). The difference is that you have to add a definition and name it.

    orthoxerox 18 hours

    I mean that Cat, Dog and Bird don't have to inherit from the union, you can declare a union of completely random types, as opposed to saying "Animal has three subtypes, no more, no less", which is what F# does more or less.

    pjc50 21 hours

    OK then, what is the opposite of this, the adhoc union?

    Semaphor 21 hours

    I don’t know for sure, but I’m guessing something like

    (Dog, Cat) pet = new Cat();

    So without defining the union with an explicit name beforehand.

    SideburnsOfDoom 21 hours

    Well, you can do this in c#:

      var someUser = new { Name = "SideburnsOfDoom", CommentValue = 3 };
    
    
    What type is `someUser` ? Not one that you can reference by name in code, it is "anonymous" in that regard. But the compiler knows the type.

    A type can be given at compile-time in a declaration, or generated at compile-time by the compiler like this. But it is still "Compiler-verified" and not ad-hoc or at runtime.

    the type (Dog, Cat) pet seems similar, it's known at compile-time and won't change. A type without a usable name is still a type.

    Is this "ad-hoc"? It depends entirely on what you mean by that.

    SideburnsOfDoom 21 hours

    I don't follow the question. Maybe define the term that you are using?

    pjc50 21 hours

    Top comment mentioned the term without defining it, confusing me and seemingly most of the thread: https://news.ycombinator.com/item?id=47649817

    SideburnsOfDoom 4 hours

    We seem to have yet another potential meaning here : https://news.ycombinator.com/item?id=47692261

    > Cat, Dog and Bird don't have to inherit from the union, you can declare a union of completely random types, as opposed to saying "Animal has three subtypes, no more, no less"

    "Animal has three subtypes" is more like the c# "sealed" modifier on a class, meaning that subtyping is not allowed. Except in this case I guess for three existing subtypes.

  • FrustratedMonky 21 hours

    Is this the last of the F# features to be migrated into C#?

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

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

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

    pjmlp 21 hours

    Microsoft's management has always behaved as if it was a mistake to have added F# into Visual Studio 2010, and being stuck finding a purpose for it.

    Note that most of its development is still by the open source community and its tooling is an outsider for Visual Studio, where everything else is shared between Visual Basic and C#.

    With the official deprecation of VB, and C++/CLI, even though the community keeps going with F#, CLR has changed meaning to C# Language Runtime, for all practical purposes.

    Also UWP never officially supported F#, although you could get it running with some hacks.

    Similarly with ongoing Native AOT, there are some F# features that break under AOT and might never be rewritten.

    A lost opportunity indeed.

    CharlieDigital 21 hours

        > I don't really get it
    
    To me it makes sense because C# is a very general purpose language that has many audiences. Desktop GUI apps, web APIs, a scripting engine for gaming SDKs, console apps.

    It does each reasonably well (with web APIs being where I think they truly shine).

        > Was a functional approach really so 'difficult'
    
    It is surprisingly difficult for folks to grasp functional techniques and even writing code that uses `Func`, `Action`, and delegates. Devs have no problem consuming such code, but writing such code is a different matter altogether; there is just very little training for devs to think functionally. Even after explaining why devs might want to write such code (e.g. makes testing much easier), it happens very, very rarely in our codebase.

    zigzag312 21 hours

    I personally like the direction C# is taking. A multi-paradigm language with GC and flexibility to allow you to write highly expressive or high performance code.

    Better than a new language for each task, like you have with Go (microservices) and Dart (GUI).

    I'm using F# on a personal project and while it is a great language I think the syntax can be less readable than that of C#. C# code can contain a bit too much boilerplate keywords, but it has a clear structure. Lack of parenthesis in F# make it harder to grasp the structure of the code at a glance.

    dathinab 20 hours

    > big bag of stuff, with no direction.

    also called general purpose, general style langue

    > that still can't really be done in C#

    I would think about it more as them including features other more general purpose languages with a "general" style have adopted then "migrating F# features into C#, as you have mentioned there are major differences between how C# and F# do discriminated sum types.

    I.e. it look more like it got inspired by it's competition like e.g. Java (via. sealed interface), Rust (via. enum), TypeScript (via structural typing & literal types) etc.

    > Was a functional approach really so 'difficult'?

    it was never difficult to use

    but it was very different in most aspects

    which makes it difficult to push, sell, adapt etc.

    that the maybe most wide used functional language (Haskel) has a very bad reputation about being unnecessary complicated and obscure to use with a lot of CS-terminology/pseudo-elitism gate keeping doesn't exactly help. (Also to be clear I'm not saying it has this properties, but it has the reputation, or at least had that reputation for a long time)

    FrustratedMonky 20 hours

    "reputation about being unnecessary complicated and obscure to use with a lot of CS-terminology/pseudo-elitism gate keeping doesn't exactly help"

    Probably more this than any technical reason. More about culture and installed view points.

    I don't want to get into the objects/function wars, but do think pretty much every technical problem can be solved better with functions. BUT, it would take an entire industries to re-tool. So think it was more about inertia.

    Inertia won.

    DeathArrow 20 hours

    >Is this the last of the F# features to be migrated into C#? >What a missed opportunity.

    Not adding functional features to F# doesn't mean F# would have gained more usage. And if someone wants to use F#, no one is stopping him or her.

    FrustratedMonky 20 hours

    I meant, 'missed', in that the entire industry would have been better off if F# or functional programming had won out over object oriented/C# styles.

    But, that would takes, schools changing, companies changing, everything. So it was really the installed base that won, not what was better.

    We'd have to go back in time, and have some ML Language win over C++ .

    raincole 21 hours

    Union is almost a net positive to C# in my opinion.

    But I do agree. C# is heading to a weird place. At first glance C# looks like a very explicit language, but then you have all the hidden magical tricks: you can't even tell if a (x) => x will be a Func or Expression[0], or if a $"{x}"[1] will actually be evaluated, without looking at the callee's signature.

    [0]: https://learn.microsoft.com/en-us/dotnet/csharp/advanced-top...

    [1]: https://learn.microsoft.com/en-us/dotnet/csharp/advanced-top...

    twisteriffic 21 hours

    From the PoV of someone who uses C# every day those are very strange things to be upset about.

    wvenable 17 hours

    > you can't even tell if...

    In the places where that is a thing, I've never needed to care. (Which is kind of the point)

    jayd16 17 hours

    Purity is overrated. C# is a kitchen sink language but you need give credit to the language designers. Compared to C++, for example, C# feels feature rich and consistent even though it abandons purity.

    FrustratedMonky 17 hours

    I think purity matters where you can have the compiler catch problems.

    jayd16 16 hours

    C# has fantastic tooling, though. Its a hidden feature but to the credit of the language designers, I don't think they often abandon compiler features for dev features.

    For example, C# chose not to go down the route of type erasure for the sake of generics and because of that you don't get the same sort of runtime type issues that Java might have.

    owlstuffing 21 hours

    > Pulling them all into C# just makes C# seem like a big bag of stuff, with no direction.

    Agreed. Java is on the same trail.

    DarkNova6 21 hours

    Care to elaborate? I think Java is showing remarkable vision and cohesion in their roadmap. Their released features are forward compatible and integrate nicely into existing syntax.

    I work much with C# these days and wish C# had as cohesive a syntax story. It often feels like "island of special syntax that makes you fall of a cliff".

    vips7L 12 hours

    It's honestly hilarious since the person you're replying to has heavily advocated Manifold which is a compiler extension to Java that adds every little feature to the language.

    https://github.com/manifold-systems/manifold

    owlstuffing 17 hours

    [dead]

    LunicLynx 21 hours

    You aren’t giving enough credit to the careful evaluation of how this adaption is happening.

    So far everything that was added to C# very much reduces the amount of dead boilerplate code other languages struggle with.

    Really give it an honest try before you judge it based on the summation of headlines.

    dathinab 20 hours

    > reduces the amount of dead boilerplate code other languages struggle with.

    given that most of the thinks added seem more inspired by other languages then "moved over" from F# the "other languages struggle with" part makes not that much sense

    like some languages which had been ahead of C# and made union type a "expected general purpose" feature of "some kind":

    - Java: sealed interfaces (on high level the same this C# features, details differ)

    - Rust: it's enum type (but better at reducing boilerplate due to not needing to define a separate type per variant, but being able to do so if you need to)

    - TypeScript: untagged sum types + literal types => tagged sum types

    - C++: std::variant (let's ignore raw union usage, that is more a landmine then a feature)

    either way, grate to have it, it's really convenient to represent a `TYPE is either of TYPES` relationship. Which are conceptually very common and working around them without proper type system support is annoying (but very viable).

    I also would say that while it is often associated with functional programing it has become generally expected even if you language isn't functional. Comparable to e.g. having some limited closure support.

    owlstuffing 20 hours

    In isolation, yes, I agree with you. But in the context of the cornucopia of other "carefully evaluated" features mixed into the melting pot, C# is a nightmare of language identities - a jack of all trades, master of none, choose your dialect language. No thanks.

    resonancel 2 hours

    C# is a perfect example of feature envy, but because "Java sucks" C# must be the best thing ever in the world of computing. Orthogonality and coherence be damned.

    LunicLynx 19 hours

    If it’s not for you I guess that is ok. But from your comment I would also deduct that you never professionally used it. After so many different languages it’s the only one I always comeback to.

    The only things that I wish for are: rusts borrow-checker and memory management. And the AOT story would be more natural.

    Besides that, for me, it is the general purpose language.

    owlstuffing 19 hours

    General purpose != multiple dialects, that is the trouble with languages like this - C# is a tower of babel.

    DeathArrow 20 hours

    >a jack of all trades

    Yes, C# is a jack of all trades and can be used at many things. Web, desktop mobile, microservices, CLI, embedded software, games. Probably is not fitted for writing operating systems kernels due to the GC but most areas can be tackled with C#.

    pjmlp 2 hours

    Many systems programming languages with GC have existed since the 1970's, we don't seem most adoption mostly due to developer culture, and monetary issues with management.

    Izikiel43 17 hours

    > Probably is not fitted for writing operating systems kernels

    Midori would like to have a word with you:

    https://en.wikipedia.org/wiki/Midori_(operating_system)

    https://joeduffyblog.com/2015/11/03/blogging-about-midori/

    wvenable 17 hours

    > C# is a nightmare of language identities - a jack of all trades, master of none, choose your dialect language.

    I honestly have no idea where you would get this idea from. C# is a pretty opinionated language and it's worst faults all come from version 1.0 where it was mostly a clone of Java. They've been very carefully undoing that for years now.

    It's a far more comfortable and strict language now than before.

    CharlieDigital 15 hours

    I can see where he's coming from. For example, `dynamic` was initially introduced to support COM interop when Office add-in functionality was introduced. Should I use it in my web API? I can, but I probably shouldn't.

    `.ConfigureAwait(bool)` is another where it is relevant, but only in some contexts.

    This is precisely because the language itself operates in many runtime scenarios.

    pjmlp 2 hours

    dynamic was also added as part of DLR, initially designed for IronPython and IronRuby support.

    This inspired the invokedynamic bytecode in the JVM, which has brought many benefits and much more use than the original .NET features, e.g. how lambdas get generated.

    wvenable 14 hours

    I guess that's a good point. I admit haven't used or seen `dynamic` in so long that I completely forgot about it.

    But I'm not sure that's really a problem. Does the OP expect everyone to use an entirely different languages every single context? I have web applications and desktop applications that interact with Office that share common code.

    Even `dynamic` is pretty nice as far as weird dynamic language features are concerned.

    Interestingly enough `.ConfigureAwait(bool)` is entirely the opposite of `dynamic` -- it's not a language feature at all but instead a library call. I could argue that might instead be better as a keyword.

    CharlieDigital 14 hours

    It is a library call, but one that is tied to the behavior of a language feature (async/await).

    The reason I bring it up is that it is another one of those things where it matters in some cases depending on what you're doing.

    Look at the depths that Toub had to go through to explain when to use it: https://devblogs.microsoft.com/dotnet/configureawait-faq/

    David Fowl concludes in the comments:

        > That’s correct, most of ASP.NET Core doesn’t use ConfigureAwait(false) and that was an explicit decision because it was deemed unnecessary. There are places where it is used though, like calls to bootstrap ASP.NET Core (using the host) so that scenarios you mention work. If you were to host ASP.NET Core in a WinForms or WPF application, you would end up calling StartAsync from the UI thread and that would do the right thing and use ConfigureAwait(false) internally. Request processing on the other hand is dispatching to the thread pool so unless some other component explicitly set a SynchronizationContext, requests are running on thread pool threads.
        > 
        > Blazor on the other hand does have a SynchronizationContext when running inside of a Blazor component.
    
    So I bring this up as a case of how supporting multiple platforms and runtime scenarios does indeed add some layer of complexity.

    wvenable 14 hours

    > It is a library call, but one that is tied to the behavior of a language feature (async/await).

    This is a good example of C# light-touch on language design. Async/await creates a state machine out of your methods but that's all it does. The language itself delegates entirely to platform/framework for the implementation. You can swap in your own implementation (just as it possible with this union feature)

    > So I bring this up as a case of how supporting multiple platforms and runtime scenarios does indeed add some layer of complexity.

    I agree that's true. A language that doesn't support multiple platforms and runtime scenarios can, indeed, be simpler. However that doesn't make the task simpler -- now you just have to use different languages entirely with potentially different semantics. If your task is just one platform and one runtime scenario, the mental cost here is still low. You don't actually need to know those other details.

    NanoCoaster 21 hours

    Absolutely agree. Modern C# language design feels very much lacking in vision or direction. It's mostly a bunch of shiny-looking language features being bolted on, all in ways that make the language massively more complex.

    Just look at this feature: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/cs...

    Was this needed? Was this necessary? It's reusing an existing keyword, fine. It's not hard to understand. But it adds a new syntax to a language that's already filled to the brim, just to save a few keystrokes?

    Try teaching someone C# nowadays. Completely impossible. Really, I wish they would've given F# just a tenth of the love that C# got over the years. It has issues but it could've been so much more.

    pjmlp 2 hours

    I think they now have an issue getting new language features every year, this is how it comes to be.

    Metasyntactic 14 hours

    Hi there! One of the C# language designers here, working on unions. And the author of that feature :D

    So I'm happy to discuss the thinking here. It's not about saving keystrokes. It's about our decision that users shouldn't have 7 (yes 7) different ways of creating collections. They should just be able to target at least 99% of all cases where a collection is needed, with one simple and uniform syntax across all those cases.

    When we created and introduced collection expressions, it was able to get close to that goal. But there were still cases left out, leaving people in the unenviable position of having to keep their code inconsistent.

    This feature was tiny, and is really intended for those few percent of cases where you were stuck having to do things the much more complex way (see things like immutable builders as an example), just to do something simple, like adding an `IEqualityComparer<>`. This was also something that would become even more relevant as we add `k:v` support to our collections to be able to make dictionaries.

    Pay08 20 hours

    This is why I have always been leery of C# and continued using Java instead. C#s development has always seemed very haphazard and kitchen sink mentality to me.

    18 hours

    jayd16 17 hours

    > Try teaching someone C# nowadays

    Do you actually have a datapoint of someone failing to understand C# or are you just hyperbolically saying its a big language? The tooling, the ecosystem, the linting, the frameworks. Its a very easy language to get into...

    mrsmrtss 16 hours

    Exactly, we have had many interns with zero C# experience become fluent in a couple of months and those with prior TypeScript or Java experience get there even faster. A good IDE (like Rider) helps also.

    twisteriffic 20 hours

    > Try teaching someone C# nowadays. Completely impossible.

    That isn't a reasonable take. Failing to teach a language by enumerating all its features is an indictment of the instructor and not the language.

    NanoCoaster 20 hours

    I guess I overdramatized the situation a bit :) It's a passionate topic for me; as somebody who has been using C# at work for 10 years now, I'm just not happy with the direction the language has been taking.

    You're right, it's not impossible and in general it's not among the hardest languages to teach. But I would argue, it is heading that way.

    There are already so many ways to do things in C#. For example, try explaining the difference between fields and properties; sounds easy, but making it really stick is quite a challenge. And that's one of the simplest cases (and a feature I'm 100% in favor of).

    And you will have to explain it at some point, because real codebases contain these features so at some point, it'll need to be taught. Learning a language doesn't stop when you can write a simple application, it continues up until at least you're comfortable with most of its features and their practical use. The quicker one can get people to that point, the easier the language is to teach, I'd argue.

    One might also argue that learning never really stops, but that's beside the point :)

    In general, my issue isn't any specific feature. C# has many features that are non-trivial to learn but still great: value types, generics, expression trees. Source generators are relatively new and I like them! I like most of the things they're doing in the standard library or the runtime. Spans everywhere is a nice improvement, most new APIs are sensible and nice to use and the runtime just keeps getting faster every release. Great. It's more the pure C# language side I have an issue with.

    But every language has a budget of innovation and cognitive load that you can expect people to deal with, and C# is not using its budget very wisely in my opinion.

    Metasyntactic 13 hours

    > I guess I overdramatized the situation a bit :) It's a passionate topic for me; as somebody who has been using C# at work for 10 years now, I'm just not happy with the direction the language has been taking.

    You should come engage with us on this then :)

    We do all our design in the open on github. And a lot of us are available to chat and discuss all this stuff in Discord and the like :)

    > C# is not using its budget very wisely in my opinion.

    I can promise you. Every feature you think are great had similar detractors over the years. Every Single One :)

    raincole 21 hours

    > Try teaching someone C# nowadays. Completely impossible. Really, I wish they would've given F# just a tenth of the love that C# got over the years

    If they actually put effort in F#, it would have reached "unteachable" state already :)

    NanoCoaster 20 hours

    Haha, yeah, maybe :)

    I would've loved an F# that found a way to improve on the performance issues, especially when using computation expressions. That and, either, a deeper integration of .NETs native OOP subtyping, or some form of OCaml-like module system, would have been enough to make it an almost perfect language for my tastes.

    Obviously, these are big, and maybe impossible, issues. But Microsoft as a whole never really dedicated enough resources to find out. I feel for the people still working on it, their work is definitely appreciated :)

    LunicLynx 19 hours

    My knowledge on functional languages is limited, but as I understand it, it’s possible to formulate expressions that are basically NP problems? And hence impossible to speed up?

    So is it a F# issue or inherent to functional programming?

    NanoCoaster 19 hours

    AFAIK it was a much more down-to-earth thing. The implementation of computation expressions in F# compiled down to lots of function objects that were not very GC-friendly. Or something like that. To be honest, I never looked that deeply at it :)

    Looking at it, the MS docs contain something about this exact topic, so maybe it's better nowadays: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...

    Sadly, I haven't used F# for years at this point so I can't speak to the current state.

    throw234234234 10 hours

    F# has since gotten Functional State machines which make many computation expressions more efficient (https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0...). Been there a while.

    I actually think F# has received some "love" over the recent years contrary to some on this forum; that feature being an example. My view, maybe unpopular but in the age of AI maybe less so, is there is a diminishing returns to language features anyway w.r.t complexity and the use cases that new feature will actually apply for. F# in my mind and many other languages now for that matter is pretty much there or are almost there; the languages are converging. When I used F# I liked how it unified features and tried to keep things simple. Features didn't feel "tacked on" mostly with some later exceptions.

    Last time I used F# a few libraries started adopting this for their CE's (e.g. IcedTasks library, etc).

    LunicLynx 21 hours

    You are looking at it from what you know about C#, the goal is how can you reduce (delete) all this to make the language more accessible.

    For you it may be fine to write:

    List<string> strs = new List<string>();

    And sure if you have been using C# for years you know all the things going on here.

    But it shouldn’t be an argument that:

    List<string> strs = [];

    Is substentionally easier to grasp.

    And that has been the theme of all changes.

    The example you point out is the advanced case, someone only needs in a very specific case. It does not have a lot todo with learning the language.

    The language design team is really making sure that the features work well throughout and I think that does deserve some credit.

    raincole 20 hours

    > The example you point out is the advanced case, someone only needs in a very specific case

    This is exactly how C++ landed where it is now. Every time it's "you only need to know that syntax if..." well it ends up everyone has to know that syntax because someone will use it and if you're a responsible programmer you'll end up reading a lot code written from other people.

    LunicLynx 19 hours

    An unbeatable argument, really.

    But still there is a difference between learning and mastering.

    I recently helped my partner learn for her CS class, and I feel very comfortable arguing that my previous statement holds up.

    Mastering? No, in that case I agree with you.

    xnorswap 20 hours

    That's a basic example with a single level of generics too, you'd sometimes have to do things like:

        Dictionary<string, List<Tuple<string, string>>> foo = new Dictionary<string, List<Tuple<string, string>>> 
    
    Or things like:

        Dictionary<string, List<Tuple<string, string>>> foo = DoSomeWork();
    
    And you'd have to get the type right, even though the compiler knew the type, because it'd tell you off for getting it wrong. Sometimes it was easiest to just grab the type from the compiler error. ( This example is of course a bit OTT, and it's a bit of a code-smell to be exposing that detail of typing to consumers. )

    No-one wants to go back to that, and anyone who says C# is over-complicated I think is forgetting how rough it was in the earliest versions.

    While introduction of auto-typing through "var" helped a lot with that, you'd still regularly have to fight if you wanted to properly initialise arrays with values, because the syntax was just not always obvious.

    Collection literals are amazing, and now the ability to pass things into the constructor means they can be used when you need constructor parameters too, that's just a good thing as you say.

    pjmlp 2 hours

    Ah, but because they have to keep everyone happy, you can choose between var or new(), and then we need to teach both to juniors.

    hirvi74 15 hours

    One issue I have with all these syntax changes is that they are all just more overhead for one to remember. All for what though? Just to just save a few more keystrokes?

    I work on multiple applications with different versions of C# and/or Dotnet. I find it quite annoying to have to remember what syntax sugar is allowed in which versions.

    If C# did not want verbose syntax, then Java was a poor choice to imitate.

    pjmlp 2 hours

    C# only exists because Sun did not allow Microsoft to keep using J++.

    Without the lawsuit, COM+ Runtime (aka .NET) would have used J++, as originally designed in the Ext-VOS paper.

    NanoCoaster 20 hours

    I'm 100% on board with the [] syntax. I'm not on board with adding the syntax for passing arguments to the constructor within that syntax.

    I agree that = [] is perfectly fine syntax. But I would definitely argue that:

    [with(capacity: values.Length * 2), ..

    is non-intuitive and unnecessary. What other language is there that has this syntax? Alternatively, is this a natural way of writing this? I wouldn't say so.

    My main language in my free time is Rust, a few years ago it was F#. So, I'm absolutely open to other syntax ideas. But I feel that there has to be a direction, things have to work together to make a language feel coherent.

    Another example would be Clojure, which I started learning a few months ago (before we all got swept up in AI FOMO :D). Clojure as a language feels very coherent, very logical. I'm still a beginner, but every time I learn something about it, it just makes sense. It feels as if I could have guessed that it works this way. I don't get that feeling at all in many of the new features of C#.

    > The example you point out is the advanced case, someone only needs in a very specific case. It does not have a lot todo with learning the language.

    I disagree. When learning the language, you're going to have to read other people's code and understand it. It's the same basic principle, but, I'd argue, much worse in C++. Yes, in theory, you don't have to understand SFINAE and template metaprogramming and (now) concepts and all those things. You could just work in a subset of C++ that doesn't use those things. But in practice, you're always going to have issues if you don't.

    HauntingPin 20 hours

    Isn't this just another form of Python's list comprehensions?

    https://docs.python.org/3/tutorial/datastructures.html#list-...

    I'm also not sure that something not being intuitive or natural is necessarily a bad thing in of itself. You state it as if it's so, but you haven't demonstrated that this way of defining a list is worse. You also haven't made any attempt to understand any possible benefit, nor have you attempted any sort of analysis comparing the good and the bad aspects.

    NanoCoaster 20 hours

    No, this is just a constructor call, it's purely syntax sugar for the new() way of doing it.

    > I'm also not sure that something not being intuitive or natural is necessarily a bad thing in of itself. You state it as if it's so, but you haven't demonstrated that this way of defining a list is worse.

    I would argue that a language having more features, without the feature being helpful, is a bad thing in itself. If the syntax isn't necessary or very convenient in many cases, it shouldn't exist. The syntax being natural (which, absolutely, is a very subjective thing) just makes it less of an issue, I'd say.

    Every new syntax added to the language adds cognitive overhead to readers of code. But also, it adds possible interactions with other language features that may be added in the future. Now, the example I brought up doesn't really concern the second point, I'll concede that. But unions? That is a big concept to add to a language that already has decades of existing conventions and tons of other features. How will they interact with generics? Nullable reference types? And, just as importantly: How will they interact with any other features that might be added at some point that we don't even know about?

    I'm not against adding syntax sugar. For example, I quite like primary constructors, which is another relatively new C# feature. I think it's a bit annoying that they were kind of added in a roundabout way, by first adding records and then adding primary constructors to classes, but this time they don't define properties but fields...but in the end, it's a nice convenience feature when using constructor injection. Which, whatever one may feel about this, is pretty common in C# code.

    But the thing is: If every single feature that's nice for a few use cases gets added to a language, the language will explode. The best example for this is C++. C# is definitely not that bad, far from it, but my point is that I want it to stay that way :)

    Metasyntactic 13 hours

    Hi there. Designer of this feature :D

    > is non-intuitive and unnecessary.

    intuitive is definitely in the eye of the beholder. When people saw:

    `HashSet<string> people = [with(StringComparer.CaseInsensitiveComparer), .. group1, group2]`

    they found it understandable. And this was also much nicer than what they'd have to write today (which would bring them out of the nice declarative collection-expression space).

    Does that make it 'necessary'? Ultimately that's up to the individual. We felt like it was. Not being able to do simple things like this felt like a 'bitter pill'. Customization of collection construction is common (looking in codebases, it shows up about 7% of the time). So having to 'fall out' from the uniform collection-expr system into the much more verbose and clunky forms just for this common enough case felt 'necessary' to us.

    >But I feel that there has to be a direction, things have to work together to make a language feel coherent.

    I feel like this is conflicting feedback. Collection expressions made the language more coherent. Instead of 7 different ways of doing things (some of which were genuinely not efficient), we gave one uniform way of doing it. That makes things more coherent. Making it so you don't have to drop out of that for something as simple as configuring the collection makes things more coherent.

    NanoCoaster 3 hours

    Hi, thanks for answering :)

    > Collection expressions made the language more coherent. Instead of 7 different ways of doing things (some of which were genuinely not efficient), we gave one uniform way of doing it.

    I see your point on this. My dislike comes from a mixture of "I don't like how it looks" and "this language already has tons of features".

    In terms of looks, I wish it could be more coherent with existing syntax.

    List<int> = new {1, 2, 3} and List<int> = {1, 2, 3} are obviously taken up by anonymous types and blocks themselves. Would something like

    List<int> = new(capacity: 10)[1, 2, 3]

    have been possible? It feels like a combination of target-typed new and the initialization syntax. It involves the "new" keyword, which everybody already associates with constructor calls. It's short. Obviously, I don't know if this even works, maybe there's a parsing issue there (aren't those the most annoying issues in language design haha).

    > they found it understandable

    Kind of in my experience. Me and the people I've shown this to can easily remember it, but we all agree that it doesn't look like obvious syntax to them. Those two things are quite different to me. Contrast this to something like target-typed new, which immediately made sense to the same people. One might argue that that's fine enough and maybe it is, but I think, the less I have to remember about a language's syntax, the better. I'm going to have to remember many many other things anyway, better keep my memory free for the details of SynchronizationContext and async flow :)

    I'm obviously aware that you get tons of bikeshedding comments like this all the time, so I'm sure you've gone through this. But to me, this invented syntax would have been fine. I just don't like the one that actually got in.

    Now, the necessity on the other hand: May just be the company I'm working at, but my personal experience has never been that this is a big issue. Sure, it's nice to not have to fall back to explicit initialization a few more times. But personally, this doesn't pass my threshold of "painful enough to warrant additional syntax".

    That's the core of my issue: Most, maybe all, of the new features in the language are fine to me in isolation. I may bikeshed about the explicit syntax (see: this thread). But my main issue is that the sum of complexity in the language and the issues beginners have when learning it are steadily increasing. I see this all the time at work.

    As you said, this is definitely subjective. And in the end, language design is a very subjective process and maybe C# just won't be for me in the long run. But I wish it would, because at its core I like it, and .NET, a lot. Which is why I will continue to speak for my (subjective) viewpoint.

    Well, this turned into a bit of an incoherent rant. I appreciate you exposing yourself to the HN acid pit ;)

    electroly 11 hours

    Practically speaking, I've found that Claude never uses collection expressions, so the feature has disappeared from my code. Before AI, the feature was looked at with skepticism by my coworkers. We like writing "var" for all variable declarations. You have to write the type on the left side if you want to declare a variable with a collection expression, and we would never do that otherwise. Can't do `foreach (var x in [1, 2, 3])`. Too often, you have to make specific accommodations in your code to allow the collection expression to be valid.

    Collection expressions today are more the sort of thing that a code poet or golfer can do to prettify their code than something a newbie can count on using. It's tough to explain "you can only use this when the collection type is implied in that spot" to a newbie. The value of the base feature is still unproven for me. I'm not sure I agree, without some convincing, that collection expressions made the language more coherent rather than doing https://xkcd.com/927.

    pjmlp 2 hours

    I equate this language addition to the same mistake that !! was going to be a few years ago, until it got all that discussion threads on Twitter.

    C# doesn't need to have syntax sugar for every possible use case.

    Some of the more recent features feel like the outcome of the team pressure to have new language features to announce in November every year.

    grandpoobah 8 hours

    As someone who has been coding C# since the pre-generics days, this is the first syntax change which I strongly disagree with. I pretty much love every little bit of syntactic sugar you guys have added to the language. But this? This seems objectively illogical and just straight up ugly. It blows my mind that this is making it into the language, and it makes me worry about the future of C#.

    Metasyntactic 4 hours

    How do you define "objectively illogical" here?

  • gib444 21 hours

    Is C# a great language trapped in a terrible ecosystem? ie would masses use C# if it existed in another ecosystem?

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

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

    jayd16 17 hours

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

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

    npodbielski 19 hours

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

    throwuxiytayq 20 hours

    It's a very nice language embedded in a very nice ecosystem. There is no catch, really.

    sakopov 16 hours

    Why is the ecosystem bad? I haven't ran any .net code on anything but Linux in years. The open source community is great. I don't know why it gets a bad rep.

    delta_p_delta_x 17 hours

    > terrible ecosystem

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

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

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

    ux266478 16 hours

    > but sadly it is saddled with the perception that it is Windows-only, which hasn't been true for a decade

    In my experience it does not work very well outside of the sanctioned Linux distributions. Quirky heisenbugs and nonsensical crashes made it virtually unusable for me on Void. I doubt that's changed in the years that have since passed.

    > not necessarily a negative, because Windows is a decent OS

    Is a language runtime worth an operating system? I think that's a paradigm we left behind in the 1970s when the two were effectively inseperable (and interwoven with hardware!) I wouldn't expect someone to swap to using a Unix system because they really want a better Haskell experience.

    I just don't see any actual interesting or meaningful reasons to care about .NET, I effectively feel the same way about it that I do about Go. Just not something that solves any problem I have, and doesn't have anything that interests me. Although effectively I did try it, so it's a moot point considering that's one of the outcomes you're wishing for.

    WorldMaker 14 hours

    > In my experience it does not work very well outside of the sanctioned Linux distributions. Quirky heisenbugs and nonsensical crashes made it virtually unusable for me on Void. I doubt that's changed in the years that have since passed.

    It's open source. Did you follow the spirit of Linux to file a bug report of as much sense of the crashes as you could make? Most OSS only supports as many distros as people are willing to test and file accurate bug reports (and/or scratch the itch themselves and solve it). It seems a bit unfair to expect .NET to magically have a test matrix including every possible distro when almost nothing else does. (It's what keeps distro maintainers employed, testing other people's apps, too.)

    It probably has gotten better since then, for what it is worth. .NET has gotten a lot of hardening on Linux and a lot of companies are relying on Linux servers for .NET apps now.

    At the very least there are very tiny Alpine-based containers that run .NET considerably well and are very well tested, so Docker is always a strong option for .NET today no matter what Linux distro you want on the "bare metal" running Docker.

    gf000 3 hours

    > Most OSS only supports as many distros as people are willing to test

    Linux distros don't differ too significantly from each other nowadays (systemd plus a different package manager most of the time), so I'm almost sure this is not the source of problems.

    Nonetheless, I can only add that we have ridiculous slowdowns in some standard library network calls on Linux, and at that point it is just not true that it will "seamlessly run on Linux", unfortunately.

    JCTheDenthog 21 hours

    Depends on what you mean by ecosystem, it hasn't been trapped on Windows for about a decade now. The variety of third party libraries available is quite good, while the standard library is robust enough that you don't need NPM nonsense like LeftPad and IsEven and IsNumber.

    Are there particular things about the ecosystem that you worry about (or have heard about)? Biggest complaint I would have is that it seems like many popular open source libraries in the .NET ecosystem decide to go closed source and commercial once they get popular enough.

    gib444 21 hours

    Yup, the commercial libraries. That's pretty big. It's nice the standard library has lots of goodies, but I doubt many projects in reality are zero-dependency

    (The amount of times I hear "the standard lib is great!" seems more to attempt to defend the plethora of commercial libraries, more than anything)

    The community feels rather insular too? The 9-5 dayjob types with employers who don't understand or embrace open source? At my age I can respect that though

    And is Postgresql a 2nd-class citizen? If so, your boss will tell you to use SQL Server surely?

    I guess it's hard to get a grasp on the state/health of .NET as to me it seems 99.99999% of the code is in private repos companies, as it's not a popular choice for open source projects. Which itself seems like a proxy signal though

    jayd16 17 hours

    The anemic open source projects are really from the lack of good cross platform support early on. That's changed now but it missed out on a time of rapid OSS expansion that Java and other took in.

    It is what it is but I wouldn't say its actually the fault of the language, especially now.

    celeries 20 hours

    I work with .NET for my day job and my team doesn't use any commercial libraries. I haven't felt limited in any sense by the .NET ecosystem. Nearly everything is open-source, too.

    vortegne 14 hours

    exactly the same experience here

    CharlieDigital 20 hours

        > And is Postgresql a 2nd-class citizen?
    
    No, it is not.

    Microsoft maintains the Npgsql project[0] and I say that it is a very capable, feature rich adapter.

    I have not used C# with SQL Server in almost a decade.

    [0] https://www.npgsql.org/

    JCTheDenthog 18 hours

    Also the recentish addition of multiple line string literals makes dealing with Postgres's case sensitivity a lot easier to manage.

    CharlieDigital 21 hours

    C# is a language that serves many masters and if you trace the origin of its featureset, you can see why each was created. Take the `dynamic` keyword: created to support interfacing with COM interop easier[0].

    It serves many audiences so it can feel like the language is a jack of all trades and master of none (because it is) and because it is largely backwards compatible over its 20+ years of existence.

    That said, I think people make a mountain out of a molehill with respect to keyword sprawl. Depending on what you're building, you really only need to focus on the slice of the language and platform you're working with. If you don't want to use certain language features...just don't use them?

    I think it excels in a few areas: web APIs and EF Core being possibly the best ORM out there. For me, it is "just right". Excellent platform tooling, very stable platform, very good performance, hot reload (good, but not perfect), easy to pick up the language if you already know TypeScript[1]; there are many reasons it is a good language and platform.

    [0] https://learn.microsoft.com/en-us/dotnet/csharp/advanced-top...

    [1] https://typescript-is-like-csharp.chrlschn.dev/

    mattgreenrocks 18 hours

    I’m convinced the comment section hates multi-paradigm languages because you can misuse them. And it has features that may not be needed, which triggers this weird purist mentality of, “gee, it would be so much better if it didn’t have feature X.” But oftentimes that’s just pontification for its own sake, and they aren’t really interested in trying it out. Feature X remains something they won’t use, so it should go.

    WorldMaker 14 hours

    > C# is a language that serves many masters and if you trace the origin of its featureset, you can see why each was created. Take the `dynamic` keyword: created to support interfacing with COM interop easier.

    VB.NET's Object was created to support interfacing with COM interop easier. VB.NET's one key niche versus C# for many early years was COM interop through Object.

    C#'s dynamic keyword was more directly added as a part of the DLR (Dynamic Language Runtime aka System.Dynamic) effort spurred by IronPython. It had the side benefit of making COM interop easier in C#, but the original purpose was better interop with IronPython, IronRuby, and any other DLR language. That's also why under the hood C#'s dynamic keyword supports a lot of DLR complexity/power. You can do a lot of really interesting things with `System.Dynamic.IDynamicMetaObjectProvider` [1]. The DLR's dependency on `System.Linq.Expressions` also points out to how much further in time the DLR was compared to VB.NET's Object which was "just" the VB7 rename of VB6 Variant originally (it did also pick up DLR support).

    The DLR hasn't been invested into in a while, but it was really cool and a bit of an interesting "alternate universe" to still explore.

    [0] https://learn.microsoft.com/en-us/dotnet/api/system.dynamic....

    DeathArrow 19 hours

    >It serves many audiences so it can feel like the language is a jack of all trades and master of none (because it is)

    That's why I like it so much. And now, I can write mostly functional code.

    >I think it excels in a few areas: web APIs and EF Core being possibly the best ORM out there

    It's awesome for web stuff and microservices.

    CharlieDigital 19 hours

        > It's awesome for web stuff and microservices.
    
    The gRPC platform support is top notch and seamless and Aspire is just :chefs_kiss:

    gib444 20 hours

    > EF Core being possibly the best ORM out there

    Is it good at the wrong thing? Eg compare to strongly-typed query generators

    CharlieDigital 20 hours

    It is a strongly-typed query generator?

    gib444 19 hours

    I meant code generators like sqlc

    CharlieDigital 19 hours

    Then this goes back to your question:

        > Is it good at the wrong thing?
    
    No, it's good at the right thing which is allowing developers to write type-safe SQL queries using C# at the application layer versus writing SQL that gets translated into C#.

    mattgreenrocks 18 hours

    Yep, up there with ActiveRecord as the finest ORM I’ve ever used. What seals it for me is the low coupling it imposes on entities.

    gib444 17 hours

    I don't think you were aware of code gen SQL tools before this conversation right

    CharlieDigital 15 hours

    What's the relevance here? Some sort of weird "Ha! Gotcha!" I'm certainly aware of code to SQL and SQL to code generators as generalized techniques, but I've not used SQL to code generators because these are not practical for most teams in the domain spaces where I operate.

    Your original quote, verbatim:

        > Eg compare to strongly-typed query generators
    
    "strongly-typed query generators" not "strongly-typed command generators" nor "strongly-typed code generators".

    EF is precisely a code to structured query language (SQL) query generator and not a query to code generator.