Back to Csharplang

DU 2022 10 24

meetings/working-groups/discriminated-unions/DU-2022-10-24.md

latest3.9 KB
Original Source

Notes for October 24th, 2022

This meeting distilled the previous use case discussions to a few broad concepts of unions, and we talked about some basic features and tenets that we want to see in unions.

Concepts

  • Result<TResult, TError>/Option<T>
    • Very interested in syntactic shortcuts here.
    • Monads over the T inside
    • Forces users handle errors up front
    • Is short declaration of these types of unions necessary?
      • Maybe not? Maybe if the BCL ships Result/Option itself, people may not actually need to declare many of their own versions of these.
  • Internal State with associated data
    • UI States
    • BoundNode
    • Subsets of these are often useful: State machines only transitioning from X -> Y, not Z -> Y, or only accepting certain types of expressions.
      • Anonymous definition of these might be useful. (BoundLiteral | BoundMethodGroup) inline, for example
    • Simplifies handling of state and state transitions, potentially enforces explicit handling of cases.
  • OneOf<T1, ..., Tn>
    • Passing heterogeneous data around
    • Anonymous definition might also be useful
      • How would this reconcile with a homogeneous subset?
    • Potentially easier to integrate into existing codebases

Union versioning and compat

  • What's our compat strategy for public APIs with this type of union in them?
  • Matters particularly in the same areas as variance: what happens when public apis start returning a new result?
  • Or stop accepting a result variant?
    • Maybe this one doesn't matter as much?
  • Source compatibility in an return position is not guaranteed when revving a union type
  • Binary compatibility in a input position?
    • Historically, we've cared about this
    • If my public function accepts a new case, all existing code should still work
    • On the other hand, if I add a new input to some interface a consumer is implementing, they may get a case they weren't expecting
  • In general, we're not worried about maintaining binary compatibility. We just want to make sure that API authors can be intentional about when they want to make a breaking change and make it obvious that they did.
  • Do we want to ensure that binary breaks happen when users encounter new cases?
    • No. If it falls out then it falls out, similar to required members.

Opaque unions

  • F# has these:

    fs
    type C =
        private // or internal
        | Case1 of int
    

    These create an interchange type that appears as a union to the file or assembly that created them, but is opaque to consumers. Do we need this?

    • We think this is probably just existing base types/interfaces in C#

Exhaustiveness

  • List all valid subtypes in the base type?
    • Compiler error when adding a new type?
    • This is Java's approach in JEP 409 - Sealed Classes
  • For sufficiently large hierarchies, maybe exhaustiveness isn't always as useful as hoped?
    • May often be working on subsets, and forcing handling of all unexpected values might erode desired errors when adding new expected cases.

Conclusions

  • Subsets of larger unions keep coming up in our discussions, though we need to make sure we're not being overly colored by roslyn.
    • Subsets of unions may call for an anonymous union syntax.
    • Having different implementation details for sets of homogeneous data and sets of heterogeneous data would likely be confusing for users. A singular lowering for subsets is likely needed.
  • Compatibility with existing forms is important. Users should not have to perform large refactorings to take advantage of any improvements we come up with here.
    • Importantly, we're not looking to deprecate existing forms of creating hierarchies.
  • Experiencing source breaks when adding or removing new cases to unions is likely both unavoidable and desirable. We're ok with binary breaks as well, but won't be attempting to ensure that they occur.