Comment by Quarrelsome

21 hours ago

I mean yes, but also: uh-oh. I'm looking forward to reading some code that is even more confusing than the code I'm already reading.

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

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...

  • 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.

    • 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.

      1 reply →

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.

  • > 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.

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.

  • 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?

    • I think it's almost always about making code more concise and programming more ergonomic. Assembly could already solve all the problems higher-level languages can solve. Yet we didn't discard them as useless.

    • Object-oriented polymorphism (interfaces, inheritance) is for when you have a fixed set of methods to implement but an unbounded set of types that may want to implement them.

      As a consumer, you cannot change the methods, but you can add a subtype. When you subtype an abstract class or an interface, the compiler does not let you proceed until you have implemented all the methods.

      Discriminated unions are for the exact opposite situation, when you have a fixed set of subtypes, but unbounded set of methods to implement on them. As a consumer, you cannot add a subtype, but you can add a new method. When you write a new method, the compiler does not let you proceed until you have handled all the subtypes.

      Good languages should support both!

      The best example is abstract syntax trees, the data types that represent expressions and statements in a programming language. "Expression" breaks down into cases: integer literal, string literal, variable name, binary operations like add(expr1,expr2), unary operations like negate(expr), function call(functionName, exprs), etc.

      Clearly all of these expression subtypes should belong to a base type `Expression`. But what methods do you put on `Expression`? If you're writing a compiler, you have to walk this syntax tree many times for very different purposes. First you might do a pass on it where you "de-sugar" syntax, then another pass where you type-check it and resolve names in the code, then another pass where you generate assembly code from it. Perhaps your compiler even supports different backends so you have a code-gen path for x86, another for ARM, etc. You'll likely want a pretty-printer so you can do automatic reformatting, maybe you want linting support, etc.

      If you look at all those concerns and say that each subtype of `Expression` must implement methods for each one, then you end up with untenable code organization. Every expression subtype now has a huge stack of methods to implement all in one file, dealing with stuff from totally different layers of the compiler. It's a mess.

      It's much cleaner to have the "shape" of the expression defined in one place without all that clutter, and then in each of those areas of the code you can write methods that consume expressions however they need, so each of those separate concerns lives in its own silo.

      ------------------------------------------------

      Some real code (but it's F# not C#) to look at.

      AST for my SQL dialect: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Typechecker code: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Backend code that outputs MS TSQL from it: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez...

      ------------------------------------------------

      If you're an old hand at OO you may be familiar with its actual answer to this problem, the "Visitor" pattern. See System.Linq.Expressions.ExpressionVisitor. However, once you've used a language with good union and pattern matching support, this feels like a clunky hack. Basically the mirror image of a language without real object orientation imitating it by passing around closures and structs-of-closures.

      ------------------------------------------------

      It doesn't just have to be compiler stuff. A business app data model can use this too. Instead of having:

          public class DbUser
          {
              public EmailAddress Email { get; set; }
              public PasswordHash? Password { get; set; } // null if they use SSO
              public SamlEntityProviderId? SamlProvider { get; set; } // null if they use password auth
          }
      
      

      You could have:

          type UserAuth =
              | PasswordAuth of PasswordHash
              | SSOAuth of SamlIdentityProviderId
      

      The implementation details of those different auth methods, the UI for them, etc. don't have to be part of the data model. We do have to model what "shapes" of data are acceptable, but "doing stuff" based on those shapes is another layer's problem.

      1 reply →

    • 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.

      6 replies →

    • 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.

      4 replies →

  • > Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.

    "Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has them.

You don’t see the use case for… unions? I’ve got to stop reading the comments. It’s bad for my health.

  • 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.

    • 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

      2 replies →

    • 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.

      3 replies →

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.

Have you considered trying them out (maybe in F#) to understand why they are so popular in many other languages?

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.

  • 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?

    • oo and support for exceptions, in particular checked exceptions, was a mistake of the 90s. We know better today, there’s a reason for why modern languages like go/rust/swift don’t use them, and why many use c++ with exceptions disabled.

      1 reply →

I've never been confused by language features. Usually the architecture or extreme indirection of the code is the confusing part.