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: Record Enum Types #6739

Closed
alrz opened this issue Nov 12, 2015 · 96 comments
Closed

Proposal: Record Enum Types #6739

alrz opened this issue Nov 12, 2015 · 96 comments

Comments

@alrz
Copy link
Contributor

alrz commented Nov 12, 2015

Record Enum Types

Currently, enum types wouldn't be considered as complete patterns and they don't have any "record" syntax to be used in pattern matching. This proposal tries to fill this gap.

This proposal won't affect regular enum types like #3704, rather, it suggests an enum-like syntax for declaring flat hierarchies of ADTs (with both value and reference types).

Enum structs

Enum structs would be more like Java enum types, for example

public enum struct Color(int R, int G, int B) {
    Blue(0,0,255),
    Green(0,255,0),
    Red(255,0,0)
}

would translate to

public struct Color {
    public readonly static Color Blue = new Color(0, 0, 255);
    public readonly static Color Green = new Color(0, 255, 0);
    public readonly static Color Red = new Color(255, 0, 0);

    private Color(int R, int G, int B) {
        this.R = R;
        this.G = G;
        this.B = B;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }
}

Another example from Java docs:

public enum struct Planet(double Mass, double Radius) {
    Mercury (3.303e+23, 2.4397e6),
    Venus   (4.869e+24, 6.0518e6),
    Earth   (5.976e+24, 6.37814e6),
    Mars    (6.421e+23, 3.3972e6),
    Jupiter (1.9e+27,   7.1492e7),
    Saturn  (5.688e+26, 6.0268e7),
    Uranus  (8.686e+25, 2.5559e7),
    Neptune (1.024e+26, 2.4746e7),
    Pluto   (1.27e+22,  1.137e6);

    public const double G = 6.67300E-11;

    public double SurfaceGravity =>
        G * Mass / (Radius * Radius);

    public double SurfaceWeight(double otherMass) =>
        otherMass * SurfaceGravity;
}

This struct must not be instantiable, because of the completeness of the pattern.

Enum classes

Enum classes are useful for declaring flat hierarchy of ADTs (similar to F# discriminated unions). For example. the following

public sealed abstract class Option<T> {
    public sealed class Some(T Value) : Option<T>;
    public sealed class None() : Option<T>;
}

could be written as

public enum class Option<T> {
    Some(T value : Value),
    None;
}

With enum classes, abstract sealed would be considered as an advanced feature where you can declare more complicated ADTs.

Remarks

  • Each member translates to a subclass of the Option<T> in this example.
  • Enum classes are implicitly abstract and cannot extend any other types.
  • Enum class members can have a body to potentially override methods on the base (like Java).
public enum class Expr {
  Const(double Value)        { public override string ToString() => Value.ToString(); },
  Mul(Expr Left, Expr Right) { public override string ToString() => $"({Left} * {Right})"; },
  Add(Expr Left, Expr Right) { public override string ToString() => $"({Left} + {Right})"; },
  Div(Expr Left, Expr Right) { public override string ToString() => $"({Left} / {Right})"; },
  Sub(Expr Left, Expr Right) { public override string ToString() => $"({Left} - {Right})"; },
}
@HaloFour
Copy link

I think there'd be a lot of potential with treating "class/struct enums" as ADTs. As a syntax I think it would be intuitive to users already familiar with C# enums. In fact I think I'd be quite happy if said "enums" would considered specifically in how they would benefit in combination with pattern matching and ADTs rather than just copypasta of Java's implementation.

@bbarry
Copy link

bbarry commented Nov 13, 2015

A minor quibble, neither of these are "complete" as I can make any sequence of 96 bits into a Color struct fairly easily (the simplest being default(Color)) and null is a valid value for this Option<T> type.

That said, I can do the same thing with an int backed enum today so the struct issue isn't a big deal breaker. And it seems if we simply accept the fact that null is a valid value for a reference type, the notion of completed reference types is entirely solvable.

This sort of gets to the root of my problem with the let ... else (mind you not the let ... when ... else form) deconstruction statement and the notion of checking for completeness in switch and match structures. I'm not convinced the set of them that can be proven during compile time is worth using. Type completeness checking seems to be a goal post on a field different than the one .NET is playing in. It may work in F# sometimes if you ignore interoperability (and enough of the BCL) and treat the program as a closed system, but in C# dealing with things like exposing a method that contains a switch statement to another assembly, the "problem" of avoiding null and proving completeness rapidly approaches intractability.

@gafter
Copy link
Member

gafter commented Nov 13, 2015

@HaloFour
Copy link

@gafter Well I guess there goes that idea (along with #3704). I assume that patent is Oracle's now.

@MgSam
Copy link

MgSam commented Nov 13, 2015

@gafter Have you guys considered it for C# and the patent is really a roadblock? I'm certain other languages besides Java have OO enums. Pretty ironic if a patent with your name on it now prevents you from doing something similar in your current role.

@HaloFour
Copy link

Actually it looks like Swift has gone this route for ADTs.

enum Barcode {
    case UPCA(Int, Int, Int, Int)
    case QRCode(String)
}

let productBarcode = .QRCode("ABCDEFGHIJKLMNOP")

switch productBarcode {
case .UPCA(let numberSystem, let manufacturer, let product, let check):
    print("UPC-A: \(numberSystem), \(manufacturer), \(product), \(check).")
case .QRCode(let productCode):
    print("QR code: \(productCode).")
}

I am certainly not a lawyer but I would suspect that such an implementation would differ from the quoted patent enough. I assume that Apple did not pay to license that patent.

@gafter
Copy link
Member

gafter commented Nov 13, 2015

@HaloFour I don't see how you read any part of that patent in Swift's language feature. Swift's enums do not have a closed set of values, they have a closed set of types.

@HaloFour
Copy link

@gafter

I don't see how you read any part of that patent in Swift's language feature.

I'm not. My assumption is that the implementation is so different as to be unrelated and therefore not infringing. But I'm not a patent lawyer nor am I terribly versed in patent law so I don't want to make too many assumptions as to how wide that Java patent could be applied. I'll defer to your expertise in that matter.

Swift's enums do not have a closed set of values, they have a closed set of types.

It seems that Swift is trying to accomplish both, or at least make it syntactically simple enough to feel like values in the simpler cases. Either way I think it accomplishes closed-ADTs in a fairly intuitive syntax.

@alrz
Copy link
Contributor Author

alrz commented Nov 13, 2015

@gafter So that is just concerning enum struct (closed set of values) not enum class (closed set of types)?

@gafter
Copy link
Member

gafter commented Nov 13, 2015

@alrz Yes, although Java's enums are actually reference types.

@alrz
Copy link
Contributor Author

alrz commented Nov 14, 2015

@gafter And that's because Java doesn't have user-defined value types yet 😄 By the way, I found the Java implementation confusing, because, as you said, it's a "closed set of values", but at the same time, you can override methods for every enum member. Then it becomes a closed set of types (or more precisely, a closed set of instances of various subclasses of the enclosing type).

This proposal clearly distinguishes these two concepts with enum struct and enum class. So you can not override methods in the former, because in that case, every enum member is solely a singleton instance of the enclosing type.

@gafter
Copy link
Member

gafter commented Nov 14, 2015

@alrz In Java, the (static) types of the members are the type of the enum.

Using enum struct to mean a closed set of values and enum class to mean a closed set of types is not clear at all. Shouldn't the features of struct vs type (on one hand) and closed set of values vs types (on the other hand) be orthogonal?

@alrz
Copy link
Contributor Author

alrz commented Nov 14, 2015

@gafter I said it's "clear" because you can not possibly have a closed set of subtypes with struct. I don't know how to restrict this to be not confusing, but I really like enum class syntax for declaring flat ADTs as they are more common, in these scenarios I think abstract sealed classes are too much.

@gafter
Copy link
Member

gafter commented Nov 14, 2015

@alrz Yes, I like the single keyword enum better than the pair of keywords abstract sealed too. However the nesting feels uncomfortable to me because of the way one has to name the members in clients (same issue as for abstract sealed).

@alrz
Copy link
Contributor Author

alrz commented Nov 15, 2015

@gafter Not just abstract sealed but also all the other noises with inner classes, I mean, look at this

public sealed abstract class Option<T> {
    public sealed class Some(T Value) : Option<T>;
    public sealed class None() : Option<T>;
}

However, I do believe that abstract sealed classes will be useful in more complicated scenarios like when the base class has a record-parameter-list or you want to declare a tree of ADTs.

But, about nesting issue, I've been thinking about this before. There are some options and pitfalls:

  • abstract sealed classes and their subclasses can be declared in namespace level. (maybe)
  • enum class members are declared nested but emit classes in the namespace level. (no way)
  • enum class members are declared nested and emit nested classes but they are accessible in the namespace scope. (like F# discriminated unions, but this I think doesn't work for C#)
  • Extending using. (This seems a reasonable solution for this problem, but yet it seems "redundant")

I don't know which direction you guys are going to take.

@alrz
Copy link
Contributor Author

alrz commented Nov 16, 2015

@gafter I just had a thought. I wanted to open a new issue but I would rather post it here since it's related to the subject.

Parts of this already proposed but I'm suggesting a unified syntax so one can get similar effect on various contexts.

It is proposed to use using to declare "strongly typed type aliases" so that it would support generics and all the goods that come with types.

public using class EmailAddress = String;
public using struct Identity = Int32;

An optional type-parameter-list would be allowed in these declarations.

I propose using(together with enum) as a modifier so one can apply it to a class or struct

using enum class Option<T> {
    Some(T Value),
    None
}

This has the same effect that AutoOpenAttribute provides for modules in F#. So the inner types of the class would be visible in the namespace level.

var some = Some<T>(value);
var none = None<T>();

This should also work for sealed abstract or any other classes as well. The thing is, that if the outer class happen to be generic together with the inner class, all the type parameters would be specified in a single type-parameter-list, delimited with a semicolon for each type. For example

public using abstract sealed class Foo<T> {
    public sealed class Bar<U>() : Foo<T>;
}

Then

var bar = Bar<T;U>();

would be equivalent to

var bar = Foo<T>.Bar<U>();

I think this would pretty much solve the problem with nesting, What do you think?

@gafter
Copy link
Member

gafter commented Nov 16, 2015

While I agree it would "pretty much solve the problem with nesting" it feels like an awful lot of new syntax and mechanism to address the problem. I think we'd have to be very careful what things are brought into scope by the implicit using.

@alrz
Copy link
Contributor Author

alrz commented Nov 16, 2015

@gafter I don't see an "implicit" using? If by "awful lot of new syntax" you mean an using modifier and the semicolon in parameter list, I disagree, but I do agree that some weird stuff going on here!

We do carelessly bring all of the types into the scope with using directives, I don't think that would be a problem, I mean, if there were an outer class with the same name in the scope, that wouldn't be an error, but the compiler would complain when you are referring to them (as it does when types in various namespaces conflict with each other because of using directives).

@HaloFour
Copy link

@alrz Not particularly related to these proposals but I always figured that Some and None patterns would be designed in such a way as to work with all existing reference/nullable types in C# rather than introducing a new Option<T> type. That's assuming that they'd even be necessary given type patterns.

@gafter
Copy link
Member

gafter commented Nov 17, 2015

@HaloFour Unfortunately, you can have a Some<string>(null).

In any case, this isn't just about Some/None. It is about more general Algebraic Data Types, of which Some/None is just one example.

@HaloFour
Copy link

@gafter

Sure, I understand that Option<T> is only serving as an example of ADTs.

I always thought that a Some of null in a functional language was one of those anomalies generally resulting from poor interop with non-functional languages or other hacky stuff. I don't think I've ever seen any Scala of F# code written to defend against Some being null. And if None/Some were in consideration for C# (and I mentioned that seems redundant given type patterns) I think I'd prefer to see them designed in such a way to be compatible with existing reference/nullable types, implemented as custom is operators.

Anyway that's all a tangent to the proposal of using enum-like syntax to describe ADTs.

@orthoxerox
Copy link
Contributor

@gafter there are two ways to solve it.

One is to throw in the constructor.

Another is to hide the constructors and use a factory method that returns a None instead of a Some when given a null value. Or even merge two types into one, just like there's no special Nullable for nulls.

Yes, this will not work that well with pattern matching (unless you can use is to privately construct an instance of Some or None), but will work wonderfully with do-notation, er, LINQ.

@alrz
Copy link
Contributor Author

alrz commented Nov 17, 2015

@orthoxerox Nullability practically breaks the completeness of ADTs. While any of your solutions would solve the problem with Some<string>(null) I think #5032 is required to facilitate ADTs.

@gafter gafter added this to the C# 7 and VB 15 milestone Nov 18, 2015
@gafter gafter self-assigned this Nov 18, 2015
@gafter
Copy link
Member

gafter commented Nov 18, 2015

We are seriously considering enum class for ADTs. Along with it we would like to add additional name lookup rules (#952) for use in patterns so that you don't have to dot off the container. It would apply to normal enums too, so you could write

switch (color)
{
    case Blue: // shorthand for Color.Blue if the old name lookup rules would fail
        // etc
}

and

Option<Foo> o = ...;
switch (o)
{
    case Some(var foo): // shorthand for Option<Foo>.Some
        // etc
}

@agocke

@alrz
Copy link
Contributor Author

alrz commented Nov 18, 2015

@gafter This is just great, however I don't know if it works for generics within generics (assuming that o is an object). Doesn't it imply that type parameter list in the constructor is being inferred?

@alrz
Copy link
Contributor Author

alrz commented Feb 19, 2016

@HaloFour That's not 🍝 at all. It is what I saw useful for enum struct, I don't see why it woudn't be for enum class as well. However, if you add members to an ADT you are practically breaking completeness.

@HaloFour
Copy link

@alrz That it would, which would most likely cause match expressions to throw at runtime assuming that didn't have additional cases to force completeness like a wildcard pattern.

@HaloFour
Copy link

Actually poking into some F#-generated IL and I find it amusing that the tag is not a field, it's a property, and it's calculated through type checks, e.g.:

public int Tag {
    get {
        if (this is Foo) return Tags.Foo;
        if (this is Bar) return Tags.Bar;
        return Tags.Baz;
    }
}

And when matching against the ADT it doesn't appear to use this property anyway, it uses the IL opcode isinst which is the same as using the C# operator is. Although my cases are simple so maybe there are situations where the compiler will use this Tag property.

Not that it has anything to do with what C# may or may not do, I just find it amusing.

@alrz
Copy link
Contributor Author

alrz commented Feb 23, 2016

@HaloFour Yeah, that was weird. It doesn't always use Tag for type checks (e.g. in helper methods), even match might emit isinst. That means virtual methods are less effective than isinst?

@orthoxerox
Copy link
Contributor

@alrz I have seen a microbenchmark somewhere that showed that isinst is the fastest dispatch mechanism, faster than delegates or virtual methods.

@alrz
Copy link
Contributor Author

alrz commented Feb 24, 2016

@orthoxerox But it's a single virtual call vs multiple isinst, if that wasn't the case we wouldn't need to bother about Tag at all.

@orthoxerox
Copy link
Contributor

@alrz You'll have to ask someone like Don Syme to get the real answer. Could the JIT compiler optimize the sequence of isinst checks into a jump table that uses a single type token?

@alrz
Copy link
Contributor Author

alrz commented Feb 25, 2016

It's interesting that F# also use the "singleton" instance for empty DU members. I don't see why it can't be used for enum struct as originally proposed in this issue (along with empty records of enum class).

@Thaina
Copy link

Thaina commented Apr 29, 2016

Would like it to be the same syntax as normal enum

I mean

public enum Mode : int { A=1,B=2,C=3 }
public enum Color : Byte4 { Red=new Byte4(255,0,0,255) }

@Thaina
Copy link

Thaina commented Apr 29, 2016

I think I was propose idea of pure struct somewhere

I mean struct that composed only native valuetype (byte/int/float/bool family) and/or a struct that also pure. These kind of struct should be able to do things like int could do. Such as make it const, use as base of enum (this proposal), or use in switchcase, etc,etc,etc. That would be useful

@alrz
Copy link
Contributor Author

alrz commented May 1, 2016

@HaloFour @orthoxerox

Actually it seems that F# has some kind of threshold to generate tag field; when you have numerous DU members, then it generates something like this:

public enum class Literal {
  None,
  Integer(int),
}

public class Literal
{
  public static class Tags
  { 
    public const int None = 0;
    public const int Integer = 1;
  }

  public int Tag { get; }

  private Literal(int tag)
  {
    this.Tag = tag;
  }

  public bool IsNone      => this.Tag == Tags.None;
  public bool IsInteger   => this.Tag == Tags.Integer;

  public static Literal None { get; } = new Literal(Tags.None);

  public sealed class Integer : Literal
  {
    public int Item { get; }
    public Integer(int item) : base(Tags.Integer)
    {
      this.Item = item;
    }
  }
}

I didn't think about how this can support non-flat hierarchies though.

@gulshan
Copy link

gulshan commented Dec 24, 2016

@alrz I would like to differentiate between ADT and the typical closed set of constant values enum. Because the former denotes types while the latter denotes values/objects. I propose the following syntax for typical enum, reusing the tuple literal and current enum syntax-

public enum Planet : (double Mass, double Radius) 
{
    Mercury = (3.303e+23, 2.4397e6),
    Venus   = (4.869e+24, 6.0518e6),
    Earth   = (5.976e+24, 6.37814e6),
    Mars    = (6.421e+23, 3.3972e6),
    Jupiter = (1.9e+27,   7.1492e7),
    Saturn  = (5.688e+26, 6.0268e7),
    Uranus  = (8.686e+25, 2.5559e7),
    Neptune = (1.024e+26, 2.4746e7),
    Pluto   = (1.27e+22,  1.137e6);

    public const double G = 6.67300E-11;

    public double SurfaceGravity => G * Mass / (Radius * Radius);

    public double SurfaceWeight(double otherMass) =>
        otherMass * SurfaceGravity;
}

Beside tuples,enums will also be able to inherit classes and structs-

public struct Color(int R, int G, int B);
public enum CommonColors : Color
{
    Blue  = new Color(0,0,255),
    Green = new Color(0,255,0),
    Red   = new Color(255,0,0)
}

And, within the class/struct definition, there can be this enums, denoting values are accessed by the class/struct name. This means besideds the enum values, there can be other objects of this class/struct-

public struct Color(int R, int G, int B)
{
    public enum this
    {
        Blue  = new Color(0,0,255),
        Green = new Color(0,255,0),
        Red   = new Color(255,0,0)
    }
}

Color color = Color.Red; //Ok now

For ADT, I support your proposed syntax.

@gafter
Copy link
Member

gafter commented Jan 28, 2019

Discussion should continue at dotnet/csharplang#485

@gafter gafter closed this as completed Jan 28, 2019
@DylanSnel
Copy link

I came onto this thread via reddit, but i actually wrote a package that achieves something similar.

https://github.com/DylanSnel/EpicEnums feel free to check it out!

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