Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add Discriminated Unions using a new Type #14208

Closed
mcintyre321 opened this issue Sep 30, 2016 · 13 comments
Closed

Proposal: Add Discriminated Unions using a new Type #14208

mcintyre321 opened this issue Sep 30, 2016 · 13 comments

Comments

@mcintyre321
Copy link

mcintyre321 commented Sep 30, 2016

Although c# 7 contains various features for pattern matching, disappointingly there doesn't seem to be a proposal for adding Discriminated Union types, with exhaustive matching.

Adding this at the language level raises a lot of questions, e.g. how do you define a closed class hierarchy? Can it be done as succinctly as in F#?

The feature can be added fairly simply using a custom type, in the same way Tuple<T0, ..., TN> was added. It's not as pretty as in TypeScript, but it works:

I maintain a library which adds a OneOf<T0, ..., TN> type (although I've seen other proposals refer to an Option<T0, ..., TN> type) which has .Match<TOut>(Func<T0, ..., TOut>, ..., Func<T0, ..., TOut> methods.

    OneOf<string, ColorName, Color> backgroundColor = "Red"; //uses implicit casts to reduce overhead
    Color c = backgroundColor.Match(
        str => CssHelper.GetColorFromString(str),
        name => new Color(name),
        col => col
   );
}

If you've used DUs you will know that using these as return types e.g. public OneOf<User, InvalidName, NameTaken> CreateUser(string username) is really powerful as you can extend the types on the return type, and get a compiler warning in areas of the code which don't handle it, something you don't get with a switch or non-exhaustive pattern match.

edit: This is not a proposal to extend the language or change the CLR, merely to include a custom Type.

@lorcanmooney
Copy link
Contributor

lorcanmooney commented Sep 30, 2016

Possible dupe of #728, depending on how attached you are to giving them names.

I'd love to see support for this in C#. A "proper" implementation would also allow an IEnumerable<A> to be assigned to an IEnumerable<A | B>, but I just don't see that being possible without additional support from the CLR (see #420).

Enum-like unions have also been mentioned in the various pattern matching, completeness, and record enum types threads.

@gafter
Copy link
Member

gafter commented Sep 30, 2016

Related to #188, #6739, #8726, #8729

@DavidArno
Copy link

DavidArno commented Oct 2, 2016

@lorcanmooney ,

I'd love to see support for this in C#. A "proper" implementation would also allow an IEnumerable<A> to be assigned to an IEnumerable<A | B>

As per what was discussed in #188, one idea was that union types would be implemented like:

public abstract sealed class AOrB
{
    class A();
    class B();
}

Which would result in two types, A and B, which are both subtypes of AOrB, thus it would meet that need of IEnumerable<A> to be assigned to an IEnumerable<AOrB> (if I'm remembering my covariance rules correctly).

So all we all have to do is endless harange @gafter et al to get straight on to fully implementing unions/ADTs, records and patetrn matching right after C# 7 ships (and, ideally, releasing these features as a C# 7.1 along with an update to the next release of VS 😆)

@lorcanmooney
Copy link
Contributor

That's clever, but means that a type could only be part of a single union. You, also wouldn't be able to form unions with types from external libraries. Ultimately, using inheritance is just working around the CLR type system which hasn't really evolved since generics were added.

That said, I imagine it's something we'd have to live with, 'cause the odds of a CLR change seem to be near zero.

@alrz
Copy link
Contributor

alrz commented Oct 2, 2016

wouldn't be able to form unions with types from external libraries.

That's possible with ADTs in F#,

type Union =
  | A of TypeA
  | B of TypeB

And perhaps, in C# (#6739),

enum class Union {
    A(TypeA),
    B(TypeB),
}

Both TypeA and TypeB could be declared in external assemblies. If you don't want to specify the exact subtype in the construction (A and B), I think an implicit conversion will help.

@DavidArno
Copy link

DavidArno commented Oct 2, 2016

@lorcanmooney, true, that would be a limitation. The actual proposal allows for more than my example though. A Maybe union could be defined as:

public abstract sealed struct Maybe<T>
{
    struct None();
    struct Some<T>(T value);
}

but then it would not be possible to assign an IEnumerable<A> to IEnumerable<Maybe<A>> for example. Without a CLR change, I think your best bet for achieving your A|B type is the Either type. For example, an incomplete implementation might be:

public class Either<T1, T2>
{
    public static IEnumerable<Either<T1, T2>> AsEither(IEnumerable<T1> leftEnumeration) =>
        leftEnumeration.Select(Either<T1, T2>.Left);

    public static IEnumerable<Either<T1, T2>> AsEither(IEnumerable<T2> rightEnumeration) =>
        rightEnumeration.Select(Either<T1, T2>.Right);

    public static Either<T1, T2> Left(T1 item) => new Either<T1, T2>(item);

    public static Either<T1, T2> Right(T2 item) => new Either<T1, T2>(item);
}

...

IEnumerable<Either<A,B>> aOrBList = (some IEnumerable<A>).AsEither<A,B>();

Another possibility might be that the "extension everything" proposals for C# 7+1 might allow extension operators, which in turn might allow something like:

public static implicit operator IEnumerable<AOrB>(this IEnumerable<A> list) => 
    list.Select(a => new AOrB.A(a));

@gordanr
Copy link

gordanr commented Oct 2, 2016

type Union =
  | A of TypeA
  | B of TypeB
enum class Union {
    A(TypeA),
    B(TypeB),
}

It is not most important question regarding discriminated unions, but...
maybe it is right time to think about syntax without curly braces and semicolons, like F#.
I think it is doable retaining the spirit of C#.

@mcintyre321
Copy link
Author

mcintyre321 commented Oct 5, 2016

@alrz @gordanr @DavidArno You can achieve all this already without syntax/CLR changes via my proposal, and against types from external libraries.

public sealed class Maybe : OneOf<Some, None>{ }

or you can do them on the fly without declaring a type (anonymous unions?)

public OneOf<ContentResult, BadRequestResult> MyControllerAction 
{
    ...
}

*in my library, OneOf is currently a struct, so you actually have to use OneOfBase<A, B> which is a class.

@mcintyre321 mcintyre321 changed the title Proposal: Add A Discriminated Union Type Proposal: Add Discriminated Unions using a custom Type Oct 5, 2016
@mcintyre321
Copy link
Author

@lorcanmooney I've updated the title and description to reinforce that this particular proposal is not asking for language/CLR extensions.

@mcintyre321 mcintyre321 changed the title Proposal: Add Discriminated Unions using a custom Type Proposal: Add Discriminated Unions using a new Type Oct 5, 2016
@DavidArno
Copy link

DavidArno commented Oct 5, 2016

@mcintyre321,
I misunderstood your original post. I completely disagree that unions/ADT's can be well achieved just by adding a new type. I feel that they must be properly baked into the language to enable them to work well with pattern matching, for example, and to allow them to offer better performance than can be achieved just with types.

There are many 3rd party libraries around that offer unions, such as my own Succinc<T>. The equivalent to your code would be:

var backgroundColor = new Union<string, ColorName, Color>("Red"); 
var c = backgroundColor.Match<Color>()
                       .CaseOf<string>().Do(str => CssHelper.GetColorFromString(str))
                       .CaseOf<ColorName>().Do(name => new Color(name))
                       .CaseOf<Color>().Do(col => col)
                       .Result();

which is more verbose than the way you have done it, but that's because I offer pattern matching features, such as "where guards", rather than just type matching. But what I offer is just an interim until the language catches up. With source generators, it'll be possible to create ADT's with proper labels for use in patterns etc, but again that's not as good as having it all baked into the language.

@mcintyre321
Copy link
Author

@MadsTorgersen What are your thoughts on this? I appreciate it's not a 'first-class' way to get unions into C#, but the syntax for declaring them seems very idiomatic.

@gafter
Copy link
Member

gafter commented May 8, 2018

Discussion should continue at dotnet/csharplang#485

@gafter gafter closed this as completed May 8, 2018
@mcintyre321
Copy link
Author

@gafter, looking at #485, I don't think these closed hierarchies offer quite the same benefits as this proposal (e.g. can't close over types from other assemblies, types can't appear in different heirarchies, definition is spread over multiple files). Why close this issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants