.NET (OK, C#) gets union types

(andrewlock.net)

95 points | by ingve 1 day ago

13 comments

  • sedatk 2 minutes ago
    I've waited for union types on C# so long that I don't even care about syntax anymore. Just give us something that works. So, I appreciate the effort, I know it's taken at least a decade to get it into this shape, and much thought has gone into it. Kudos to the team.
  • jdw64 24 minutes ago
    C# is my strongest and favorite language. That said, it's frustrating that the C# framework ecosystem lacks solid options. MAUI is especially half-baked, and I'm really starting to doubt whether I should continue using XAML
    • makotech221 21 minutes ago
      Winforms, wpf, blazor, maui, avalonia, what are you talking about
      • jdw64 18 minutes ago
        Alright. I'm actually fine with WinForms and WPF since my factory floor codes depend on them. But the reality is they aren't expressive enough for modern UIs. XAML is an issue, and WPF is boilerplate hell. But then Blazor is too heavy, MAUI is broken and buggy, Avalonia is underwhelming, and WinUI 3 is an absolute nightmare."
        • sedatk 13 minutes ago
          > Avalonia is underwhelming

          How? Can you elaborate?

          • jdw64 6 minutes ago
            [dead]
      • Salgat 17 minutes ago
        What I don't get is why Java doesn't get dogged for desktop UI like C# does.
        • blanched 14 minutes ago
          Because Microsoft pushes C#/dotnet as the preferred way to write UI on Windows.
  • glimshe 1 hour ago
    I love C# and in every iteration we're getting more and more features to get C-like performance in a lot of scenarios. C# does it really well because if your problem isn't performance/memory-constrained, you can ignore these features and fallback on the language's natural ease of use.
  • munchler 1 hour ago
    F# leads the way and C# slowly catches up, as always. Yet for some reason, C# still gets all the mindshare.
    • correct_horse 1 hour ago
      Haskell, OCaml, Erlang lead the way and Rust, Zig and Go get all the mindshare. I feel like its a common pattern for more experimental languages to pioneer features and other languages to copy the features and bring them to a C style syntax that the majority of devs are familiar with.
      • cultofmetatron 52 minutes ago
        Rust and Zig brought new ideas for memory management that Haskell, OCaml, Erlang sidestep having garbage control. its honestly amazing to me that they managed to get the adoption they have while being so innovative. I say this as a fulltime elixir dev.
      • skybrian 38 minutes ago
        Being first isn't necessarily good if you get it wrong, though. Laziness by default and Hindley-Milner type inference seem like mistakes that simply aren't going to get cleaned up. Other languages make their own mistakes too.
      • jiggawatts 45 minutes ago
        I wish I could find the reference, but there was a great blog / article by a computer science academic basically saying that OO, procedural, and functional paradigms are extremes of a design space where the “middle” of its Pareto frontier was essentially unknown until recent advances.

        Moreover, many functional languages are getting pseudo-procedural features via the like of “do” syntax and monads, but that this is in some sense a double abstraction over the underlying machine that is already inherently procedural.

        Starting from a language that is already procedural and sprinkling some functional abstractions on top is simpler to implement and easier for humans to use and understand.

        Rust especially showed that many of the supposed advantages of functional languages are not their exclusive domain, such as sum types and a powerful type system.

        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.

    • nullhole 32 minutes ago
      What types of problems are better solved in F# than C#?

      Is having a combination of F# and C# in a single codebase possible? Is it recommended?

      • Akronymus 26 minutes ago
        Easy code is much easier in f#, a lot of the time. Hard code is usually easier in f# due to the type system helping a lot. F# is also a lot more concise.

        And yes, you can combine them, but afair, only in terms project boundaries. (You can include a c# project in an f# one and vice versa). There are a few cases where it's quite useful. For example, rewriting a part of a big project in f# to leverage the imperative shell - functional core architecture. Like rewriting some part that does data processing in f#, so that you can test it easier/be more confident in correctness, while not doing a complete rewrite at once.

        Sort of like rust parts in the linux kernel.

      • moron4hire 28 minutes ago
        It's very possible, even encouraged when you have workloads that call for it. F# is a great functional language, so it's good for parsers, compilers, etc. The support for units of measure is also really cool, making it great for scientific computing.
  • hahn-kev 1 hour ago
    I'm glad to finally see this making it's way into C#. Not so much because I want to use unions purely in C#. But because I want to be able to define them when interfacing with other languages.
  • rohitsriram 53 minutes ago
    F# has had this for decades, C# is basically just slowly becoming F# with a C-style syntax. Not complaining though, most teams aren't switching languages so getting these features where people actually work is better than nothing.
  • moomin 1 hour ago
    AFAICT, this means you won’t be able to define Either<string, string>, which is definitely a thing you sometimes want to do.
    • sheept 1 hour ago
      It seems like if you wrap both in a record then it should be possible:

          public record Left<T>(T Value);
          public record Right<T>(T Value);
          public union Either<L, R>(Left<L>, Right<R>);
    • jzebedee 1 hour ago
      C# is strongly-typed, not stringly-typed. The point of the union is to list possible outcomes as defined through their respective types.

      The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.

      [1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

      • nesarkvechnep 1 hour ago
        I guess C# is more strongly-typed than Haskell then... /s
    • throw1234567891 1 hour ago
      but can you define T1 and T2 of string, then use Either<T1, T2>?
      • eterm 41 minutes ago
        Could you be clearer about what you mean, since string is a sealed type in C#, so what exactly do you mean T1 and T2 of string?
        • rawling 28 minutes ago
          A record wrapping a string, indicating what the string represents, so you can't mix it up with a different thing also represented by a string.
          • eterm 16 minutes ago
            Yes, you can have two different record types which both wrap a string value.

            As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:

                var fileResult = Helpers.ReadFile(@"c:\temp\test.txt");
            
                Console.WriteLine("Extracted:");
                Console.WriteLine(Helpers.ExtractString(fileResult));
            
                public record FileRead(string value);
                public record FileError(string value);
                public union FileResult(FileError, FileRead);
            
                public static class Helpers
                {
                    public static FileResult ReadFile(string fileName)
                    {
                        try
                        {
                            var fileResult = System.IO.File.ReadAllText(fileName);
                            return new FileRead(fileResult);
                        }
                        catch (Exception ex)
                        {
                            return new FileError(ex.Message);
                        }
                    }
            
                    public static string ExtractString(FileResult result)
                    {
                        return result switch
                        {
                            FileError err => $"An Error occured: {err.value}",
                            FileRead content => content.value,
                            _ => throw new NotImplementedException()
                        };
                    }
                }
            
            
            Now, such an example would be an odd way to do things, but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.
  • deadeye 55 minutes ago
    I wish the syntax looked more like typescript. This will confuse my eyes for a while.
  • SuperV1234 9 minutes ago
    Boxed, and needs complex incantations to avoid the boxing. Meh.
  • Quarrelsome 1 hour ago
    I mean yes, but also: uh-oh. I'm looking forward to reading some code that is even more confusing than the code I'm already reading.

    Not entirely convinced that I see the usecase that makes up for the potential madness.

    -------

    EDIT: Ok so I just asked Gipity instead of wasting time chatting to smug fucks in the comments who are mostly bad at expressing themselves or only show up to crow. Thanks to everyone that did try to be helpful and I apologise for sweeping you up in that same wide brush.

    So I figure its a means of better expressing branching logic that is happy path adjacent. That makes a lot of sense to me. e.g.

        return loginResult switch
        {
            LoginSucceeded success =>
                StartSession(success.User),
    
            InvalidCredentials =>
                ShowInvalidLoginMessage(),
    
            MfaRequired mfa =>
                RedirectToMfa(mfa.ChallengeId),
    
            AccountLocked locked =>
                ShowLockedAccountPage(locked.UnlockAt),
    
            PasswordExpired =>
                RedirectToPasswordReset()
        };
    
    Where previously you'd have issues around return types without unions (e.g. mfa.ChallengeId).

    I mean you could still do this with exceptions but for MFA for example that doesn't seem right.

    A 500 error would still get caught via an exception thus preserving the usefulness of moving unexpected error logic to some place else and keeping the happy branch clean.

    So it seems very useful. If you got any other good usecases for using it elsewhere, please let me know.

    • zoogeny 39 minutes ago
      This is a classic debate in programming, literally:

      2001: "Beating the Averages" (Paul Graham) [1]

      2006: "Can Your Programming Language Do This?" (Joel Spolsky) [2]

      Both of these articles argue for the thesis that programmers that have been deprived of certain language features often argue that they don't need those features since they are already comfortable working around the lack of said features.

      It's a fancy way of arguing: you don't know what you're missing because you've never had it. Or, don't knock it until you try it.

      Consider, is your argument a) I've never used it and don't see a need for it, or b) I've used it before and didn't get any benefit?

      1. https://paulgraham.com/avg.html?viewfullsite=1

      2. https://www.joelonsoftware.com/2006/08/01/can-your-programmi...

      • Quarrelsome 27 minutes ago
        I can already do functional programming like map/reduce in C# tho. Not sure what the LISP argument is. Spolsky was saying there's a perf benefit in there somewhere but I'm not seeing how unions give me that.
        • zoogeny 21 minutes ago
          You have at least two options:

          1. Argue from ignorance. Never try unions in any other programming languages and completely disallow their use in C# codebases that you participate in.

          2. Try them out and adopt an informed opinion.

          You may even choose to remain in ignorance until someone wastes their own time trying to convince you. But it isn't my job or desire to teach someone who won't put in the effort to learn for themselves.

    • Sharlin 1 hour ago
      Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.
      • Quarrelsome 1 hour ago
        ok, so what problems do they help me solve that I can't already solve? Is it just that we can make code more concise or am I missing a trick somewhere?
        • JCTheDenthog 42 minutes ago
          Simple example that I use often when writing API clients:

          In current C# I usually do something like

          public class ApiResponse<T> { public T? Response { get; set; } public bool IsSuccessful { get; set; } public ErrorResponse Error { get; set; } }

          This means I have to check that IsSuccessful is true (and/or that Response is not null). But more importantly, it means my imbecile coworkers who never read my documentation need to do so as well otherwise they're going to have a null reference exception in prod because they never actually test their garbage before pushing it to prod. And I get pulled into a 4 hour meeting to debug and solve the issue as a result.

          With union types, I can return a union of the types T and ErrorResponse and save myself massive headaches.

          • Quarrelsome 33 minutes ago
            I think I get it but I'm not really sure what I'm gaining over exception types. With an intelligent use of exceptions I can easily specify the happy path and all the error paths separately which seems really nice to me, because usually the behaviour between those two outcomes is rather different.
            • JCTheDenthog 11 minutes ago
              Exceptions are significantly slower than normal control flow in C# (about 10,000 times slower). It's also pretty non-idiomatic in both C# and most other languages I've worked in to use exceptions instead of a switch statement or similar to handle an HTTP error code. Also there can be multiple possible non-error responses from an endpoint you need to differentiate between, and exceptions would make zero sense in that case.
        • bigstrat2003 51 minutes ago
          I think "what problems do they solve that I can't already solve" is the wrong way to look at it. After all, ultimately most language features are just syntactic sugar - you could implement for loops with goto, but it would be a lot less pleasant. I think that unions aren't strictly necessary, but they are a very pleasant to use way of differentiating between different, but related, types of value.
          • Quarrelsome 48 minutes ago
            Ok. I'm just trying to understand what code I'm replacing with them. Like I wanna see the before and after in order to gain the same level of excitment as other people seem to have for them.

            Often the explanations just seem rather abstract which makes it harder to appreciate the win, versus the hideous sort of code that might appear when they're misused.

            • airstrike 39 minutes ago
              They are so fundamental to the way I write code I can't imagine ever using a language that does not support them.

              "Make invalid states unrepresentable."

            • speed_spread 12 minutes ago
              The value is realized when you have both discriminated union types _and_ language pattern matching (not regex). Then it's not just a way to structure data but a way to think about how to process it.
    • vips7L 1 hour ago
      Union/sum types are generally a good thing. Even Java added them. They tend to be worth “the madness”. Now the rest of all the crazy C# features might be a different question.
      • dgellow 1 hour ago
        What features do you see as crazy?
        • munchler 1 hour ago
          All the weird cruft around nullability, for starters. Once again confirming that allowing null references is usually a mistake.
          • dgellow 46 minutes ago
            Do you mean the implicit nullable types? Now that you can make nullable explicit instead I really don’t have much issues with it. It is part of the type system, as it should, and you have null coalescing operators. Is it still problematic or are you dealing with older codebases where you cannot set the nullable pragma?
        • vips7L 41 minutes ago
          Maybe not crazy but the language just has a really broad surface. I find it to be like the Scala of the OO world.
    • munchler 1 hour ago
      Unions are simpler than subclasses and more powerful than enums, so the use cases are plentiful. This should reduce the proliferation of verbose class hierarchies in C#. Algebraic data types (i.e. records and unions) can usually express domain models much more succinctly than traditional OO.
      • Quarrelsome 1 hour ago
        > so the use cases are plentiful

        such as?

        > This should reduce the proliferation of verbose class hierarchies in C#

        So just as an alternative for class hierarchies? I mean good people already balance that by having a preference for composition.

        • munchler 55 minutes ago
          Simple example:

             type Expr =
                 | Primitive of int
                 | Addition of (Expr * Expr)
                 | Subtraction of (Expr * Expr)
                 | Negation of Expr
          • Quarrelsome 52 minutes ago
            Isn't that just Func<int> ?
            • afdbcreid 27 minutes ago
              Really not. You can, of course, having instead a delegate to evaluate the expression. But then that's all you can do. You can't pretty-print it, for example, or optimize it, or whatever.
        • LeFantome 43 minutes ago
          “Compoision”. A typo I know but it would be a word describing what goes wrong with class hierarchies.
    • oompydoompy74 1 hour ago
      You don’t see the use case for… unions? I’ve got to stop reading the comments. It’s bad for my health.
      • adjejmxbdjdn 52 minutes ago
        I love discriminated unions.

        The problem with C# is that it’s so overloaded with features.

        If you come from one codebase to another codebase by a different team it’s close to learning a completely new language, but worse, there is no documentation I can find that will teach me only about that language.

        Throw in all the versioning issues and the fact that .Net shops aren’t great about updating to the latest versions, especially because versions, although technologically separated from Visual Studio, are still culturally tied to it, and trying to break that coupling causes all kinds of weird challenges to solve.

        Then stuff like extensions means your private codebase or a 3rd party lib may have added native looking functionality that’s not part of the language but looks like it is.

        Finally, keywords and operators are terribly overloaded in C# at this point, where a keyword can have completely different meanings based on what it’s surrounded by.

        LLMs are a huge help here, since you can point to a line of code and ask them to figure it out, but it still makes the process of navigating a C# codebase extremely challenging.

        So I can see why someone may be unhappy to see yet another feature. It’s not just this one feature. It’s the 100s of other features that are hard to even identify.

        • paddim8 47 minutes ago
          I am all for minimalism but "If you come from one codebase to another codebase by a different team it’s close to learning a completely new language" I really don't agree. It's not that big. Just sounds like a skill issue
        • Quarrelsome 45 minutes ago
          none of that applies to my position. I have an appreciation for almost all of C# and am comfortable in the framework. I just want to know what situations would be better suited to using them than traditional approaches.

          I get there's an .Either pattern when chaining function calls so you don't have to do weird typing to return errors, but I'm using exceptions for that anyway, so the return type isn't an issue.

      • Quarrelsome 1 hour ago
        thanks for helping.
    • andix 41 minutes ago
      I've never been confused by language features. Usually the architecture or extreme indirection of the code is the confusing part.
    • weinzierl 1 hour ago
      A common use case for the sum type is to define a Result (or Either) type. Now, C# not having checked exceptions is not as much in need for one as Java is, but I could still imagine it being useful for stream like constructs.
      • Quarrelsome 1 hour ago
        yeah this is the one I've considered as being mildly compelling. But don't we lose the fun of having exception handling as separate to the happy path?
  • antonvs 13 minutes ago
    Did Anders Hejlsberg die, or something?
  • le-mark 1 hour ago
    I used to see some excitement around .net core several years ago. I haven’t heard or seen much in the wild. Is anyone using .net on systems other than windows nowadays?
    • dgellow 1 hour ago
      It’s huge in the game dev world, with Unity and Godot. .net also had a reasonable community on mobile for a while thanks to Xamarin, but I cannot imagine that many people using it for new mobile projects in 2026 (outside of game dev I mean).

      It’s a very decent language (I mean C#) and runtime, I wish it had more market share in the startup world.

      • Rohansi 36 minutes ago
        Unity is still using Mono these days which is missing basically all of the C# and .NET improvements from the past... 10 years now?

        Godot was using Mono too but has since switched to .NET in version 4.

        Still a great language and I hope Unity can hit their target to switch to .NET soon!

      • smlavine 1 hour ago
        An enterprise shop I co-op'd at was porting one of their apps from Xamarin to MAUI when I worked there, but certainly it doesn't have much mindshare (if any) amongst SE undergrads at my university.
        • unethical_ban 55 minutes ago
          Someone I know who works with .net says that there is still no replacement for full Visual Studio for development, which is Windows only.
          • dgellow 36 minutes ago
            Rider is the replacement, unless they are doing really specific (like WinUI2)
    • lol768 1 hour ago
      Yes; many (Alpine/Debian) containers in K8s on GKE for production rail ticketing infra in the UK.

      There's not tons of noise being made because for the most part it all, Just Works and that's fairly boring. Perf, memory usage etc gets better every release. As an ecosystem, I'm pretty happy with it. I reach for other languages for smaller microservices.

      • gib444 51 minutes ago
        > rail ticketing infra in the UK

        You mean Raileasy? Or RDG too? (Just curious about the stack of the wider rail tech infra)

    • bel8 1 hour ago
      I consulted for multiple enterprise C# projects in the last 5 years. At least two of them are 1mil+ lines of code each.

      All of them run in Linux servers.

      Some of them were ported from PHP and Python to C#.

      Plus LLMs thrive in strongly typed languages.

      Which means C# will keep being very strong in enterprise too. Not only in games where it reigns a large chunk of the market share.

    • forgotaccount3 1 hour ago
      Yes, lambda's and our dev's use mac's so it enables that. We deploy some apps to some unix based server as well but the company is mostly windows servers anyway.
    • yread 1 hour ago
      Wwwuuuuuaaahhhhh! (making a big wild excited noise using asp.net core exclusively on Linux servers since 2017)
    • b65e8bee43c2ed0 1 hour ago
      it was an obvious marketing campaign. back then core and blazor were shilled relentlessly, and the artificial excitement died the moment MS moved on to shill vscode and typescript.

      companies spend a lot on marketing, and it's not just ads.