dinfuehr / dora

Dora VM
MIT License
490 stars 31 forks source link

Extending the reach of enums #271

Closed soc closed 2 years ago

soc commented 3 years ago

Here is the idea; consider an enum (ADT) definition like this:

enum Foo { A(String), B }

Now, instead of A and B being definition of variants on their own, they refer to existing types instead.

This means that one needs to provide an actual (class/struct/module) definition of A and B, which has multiple benefit (and no drawbacks from what I see):

  1. Enum variants have types because they have a "real" class/struct/... declaration. (This fixes a mistake that some languages like Rust or Haskell made.)
  2. Variants can be reference or value types (because they have a "real" class/struct/... declaration). So the enum discriminator can either be a number (like before) or the ref type's vtable.
  3. No "stutter", where variant names have to be invented to wrap existing types (Rust has this issue a lot).
  4. enum values can be passed/created more easily, because there are fewer layer of wrapping.
  5. Variants can be re-used in different enums.
  6. It makes it much easier to define ad-hoc enums when needed, obviating the need for a separate union type/type alias/etc. feature in the language.
  7. Nesting enums is straight-forward.

Example for 1., 2., 3.

So while ...

enum Option[T] {
  Some(value: T),
  None
}

... would receive little benefit from being written as ...

enum Option[T] { Some[T], None }
struct Some[T](value: T)
module None

... even trivial ADTs like a JSON tree would benefit. Instead of ...

enum JsonValue {
  JsonArray(value: Array[JsonValue]),
  JsonNumber(value: Float64),
  JsonString(value: String),
  JsonBool(value: Bool),
  JsonNull,
  ...
}

... one would write (with Array, Float64 and String being existing types in the language):

enum JsonValue {
  Array[JsonValue],
  Float64
  String,
  JsonNull,
  ...
}
module JsonNull

Another example would be ConstPoolEntry:

// before                         // after
enum ConstPoolEntry {             enum ConstPoolEntry {
    Int32(Int32),                     Int32,
    Int64(Int64),                     Int64,
    Float32(Float32),                 Float32,
    Float64(Float64),                 Float64
    Char(Char),                       Char,
    String(String),                   String
}                                 }

Example for 4.

It would also do away with having to wrap data into the enum's "variant" when passing arguments, as it's done with the "traditional" approach:

fun someValue(value: JsonValue) = ...
someValue("test") // not: someValue(JsonString("test"))

Whether it is desirable to support this for arbitrarily nested enums remains to be seen.

Example for 5.

Consider a class like

class Name(name: String)

With this approach we can use this Name type multiple times in different enums (and elsewhere):

enum PersonIdentifier {
  Name,
  ... // other identifiers like TaxId, Description, PhoneNumber etc.

enum DogTag {
  Name,
  ... // other identifiers like RegId, ...

So from my perspective, this approach reduces indirection at use-site and increases the utility of enums compared to more "traditional" enums, while not changing their runtime costs or representation.

soc commented 3 years ago

Implementation notes

Compared to now, "being a enum value" needs to be thought differently, as types defined elsewhere may now be enum values, i. e.:

enum Foo { Bar(...) }; // Bar defined elsewhere
let someBar = Bar(...);
someBar is Foo; // should be true