Skip to content

Discussion: Unions/closed hierarchies/ADTs, which one does C# 7.1+ need & what would the spec look like? #75

@DavidArno

Description

@DavidArno

[Please note, I'd like to use this thread to build a proper proposal for union types in C#, so this will be edited over time to take into account feedback to build it into a proposal]

A group of related features requested many times in the Roslyn repo is for something similar to F#'s discriminated unions. The proposals use various names for these type collections: unions, ADTs (arithmetic data types), closed hierarchies, hierarchy data types etc.

In addition, numerous different syntaxes have been proposed, re-using enum. abstract and sealed to denote it's a union as well as varying greatly in the syntax within the type.

Lastly, just to add to the confusion, separate proposals for enum types similar to that supported by Java have also come and gone. These threads then often morph into discussions around unions too.

Rather than list all the existing proposals, the following two seem a useful starting point as they then link to many of the other proposals:
[C# Feature Request] Hierarchy Data Type
Proposal: Add Discriminated Unions using a new Type

Please note, this proposal doesn't cover closed sets of values, as discussed in Proposal: Record Enum Types as they potentially infringe on a Java "patent" (see that thread for details). It is only concerned with closed sets of types.

Declaring a union

An example of a union from F# for fun and profit is:

type Person = {first:string; last:string}  // define a record type 
type IntOrBool = I of int | B of bool

type MixedType = 
  | Tup of int * int  // a tuple
  | P of Person       // use the record type defined above
  | L of int list     // a list of ints
  | U of IntOrBool    // use the union type defined above

Using C# records, this could potentially be expressed in C#, using a form of union definition syntax, as:

class Person(string first, string last); // define a record type
enum class IntOrBool { int I; bool B; }

enum class MixedType
{
    (int, int) T;
    Person P;
    IList<int> L;
    IntOrBool U;
}

Constructing a union instance

As the underlying type of the above union is MixedType, it might be necessary to use the following syntax to construct them:

var tuple = new MixedType.T(1, 1);
MixedType list = new MixedType.L(new List<int> { 1, 2 });

It would be desirable to be able to express it as following though, but this could lead to name resolution issues(?):

var tuple = new T(1, 1);
MixedType list = new L(new List<int> { 1, 2 });

i,e, the MixedType part has been dropped and the "subtype" names are used directly.

Pattern matching

Using the "subtype" names directly and the upcoming match expression, a pattern match of a union might look like:

var x = tuple match (
    case T (x, y) : x + y,
    case U intOrBool when intOrBool is int i : i,
    case * : 0
);

Generics

The exemplar union type of Option<T. (chosen over Maybe<T> as the former is used by F#) could be declared as:

struct None { ... }

enum struct Option<T>
{
    None None;
    Some Some<T>(T value);
}

Obvious questions

  • What do we call them? Discriminated unions seems the obvious choice to me, as that's what F# uses. What merit would other terms bring us?
  • Is the syntax used above a good choice, or could it be expressed in a better form?
  • Can this all be achieved without CLR changes? (I'm assuming yes, as F# does it)
  • What does the union declaration get converting into? (much like records, the above syntax is effectively hiding lots of boilerplate code. What is that code)?
  • Can structs be used for unions? How would a var x = default(MixedType) handled, to ensure it results in a meaningful value?
  • What other questions need asking and answering?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions