proposal-enum icon indicating copy to clipboard operation
proposal-enum copied to clipboard

Supports for ADT enum?

Open petamoriken opened this issue 6 years ago • 12 comments

The enums in Swift, Rust and Scala (case class) can have values and it can be unwrapped by pattern matching.

// swift
enum Command {
    jump
    run(velocity: Int)
    punch(target: Enemy, power: Int)
}

let command = Command.run(20)
switch command {
case .jump:
  print("jump")
case .run(let velocity):
  print("run(velocity: \(velocity))")
case .punch(let target, let power):
  print("run(target: \(target), power: \(power))")
}

It would be more useful to support Algebraic Data Types (ADT), so I would like to include it in this proposal.

enum Command {
  jump
  run(velocity)
  punch(target, power)
}

// Stage 1 Pattern Matching
const command = Command.run(20);
case (command) {
  when Command.jump -> {
      console.log(`jump`);
  }
  when Command.run(velocity) -> {
      console.log(`run(velocity: ${velocity})`);
  }
  when Command.punch(target, power) -> {
      console.log(`punch(target: ${target}, power: ${power})`);
  }
}

// types
typeof Command.run === "function";
typeof Command.run(20) === "object";

// not necessary if implementation becomes difficult
Command.run(20) === Command.run(20);

petamoriken avatar Aug 16 '18 20:08 petamoriken

FYI: https://github.com/rustwasm/wasm-bindgen/issues/31

wasm-bindgen does not yet support enums with payload, because JS itself doesn't do so.

Tarnadas avatar Mar 18 '19 08:03 Tarnadas

@petamoriken I noticed that the Prior Art section of proposal-enum doesn't mention enums like the ones you mention in Rust, Swift, and Scala. (or the ML-family language features that inspired the Rust/Swift/Scala enums)

Even if the proposal doesn't grow to allow enums with payloads, it would be helpful for the proposal to mention them and say why they aren't included.

mheiber avatar Nov 28 '19 08:11 mheiber

@rbuckton Any thoughts on this?

Given that newer languages such as Rust, Swift, etc are all generalising the concept of enums into rich ADTs, it seems strange to base a modern JS proposal on "traditional" enums...

I can think of numerous benefits of an ADT-oriented approach over a "traditional" one, particularly in the functional programming space. Some off the top of my head:

  • Easy functional utility types
    enum Result {
        Failure(value),
        Success(value),
    }
    
    enum Optional {
        None,
        Some(value),
    }
    
  • ADT-oriented domain modelling:
    enum ContentType {
        HTML(charSet),
        JSON(charSet),
        FormData(boundary),
    }
    
    enum ContentDisposition {
        Inline,
        Attachment(fileName, fileNameStar),
        FormData(fieldName, fileName, fileNameStar),
    }
    
  • Opportunities for first-class integration with pattern matching (including potential exhaustiveness checks using something like TypeScript)

I'm not sure I'd personally support the introduction of "traditional" enums into JS, as I feel they're too primitive and don't add enough value to justify the extra syntax. In contrast, I'd be hugely in favour of introducing more modern, ADT-oriented enums to the language, as they're a more flexible construct that provides vastly superior modelling capabilities. 🙂

treybrisbane avatar Feb 22 '20 03:02 treybrisbane

I'm curious about what the benefits of JS enum-with-payload are as compared to the following pattern I see a lot in TypeScript:

type Result<Success, Failure> = {
     kind: "success",
     value: Success
} | {
    kind: "failure",
    value: Failure,
};

function handleResponseOrThrow<S, F>(result: Result<S, F>): S {
    switch(result.kind) {
         case "success":
               // value is of type Success here
              return result.value;
         case "failure":
              // value is of type Failure here
              throw Error(`failed ${result.value}`);
         default:
              // next line will error at compile time if there is an unhandled enum variant
              // (something other than "success" or "failure")
              const _exhaustivenessCheck: never = result;
              // really should be unreachable
              throw Error("unreachable");
     }
}

A JS solution could probably have more succinct pattern matching and exhaustiveness checking.

mheiber avatar Feb 22 '20 10:02 mheiber

I do hope I have ADT enum at least once a week but I think TS won't do that cause JS doesn't have that. I'm strong desire to see ADT enum in JS.

Jack-Works avatar Apr 01 '20 19:04 Jack-Works

One possibility of allowing enums with payloads would be to leverage some of the design around struct in https://github.com/rbuckton/proposal-struct and allow only wasm-primitive and struct-compatible types in the payload definitions:

enum Result {
  Failure(i32, box), // `box` currently indicates any ECMAScript language value
  Success(box)
}
const res = Result.Failure(404, "not found");

However, this needs a form of pattern matching to be truly usable.

rbuckton avatar Apr 03 '20 02:04 rbuckton

Without pattern matching, it is still useful. It can simplify the creation of the tagged union.

For example

const f = () => Result.Success(123)

const x = f()
switch (x.kind) {
    case Result.Success:
        alert(x.items[0]) // 123
}

Jack-Works avatar Apr 03 '20 02:04 Jack-Works

@rbuckton I was thinking we could leverage something similar to Scala's Extractor pattern (i.e. give enum values an unapply method that returns a tuple containing the original inputs). That would enable extraction of values in the absence of pattern matching, and could hypothetically be worked into any pattern matching implementation.

treybrisbane avatar Apr 03 '20 08:04 treybrisbane

One possibility of allowing enums with payloads would be to leverage some of the design around struct in https://github.com/rbuckton/proposal-struct and allow only wasm-primitive and struct-compatible types in the payload definitions:

enum Result {
  Failure(i32, box), // `box` currently indicates any ECMAScript language value
  Success(box)
}
const res = Result.Failure(404, "not found");

Would Failure(1, box) === Failure(1, box) evaluate to true?

mheiber avatar Apr 16 '20 11:04 mheiber

@rbuckton I was thinking we could leverage something similar to Scala's Extractor pattern (i.e. give enum values an unapply method that returns a tuple containing the original inputs). That would enable extraction of values in the absence of pattern matching, and could hypothetically be worked into any pattern matching implementation.

Ideally, there would be a custom way of extracting values, so that data structure authors can maintain their API boundaries.

unapply is one way, but view patterns are more general (could be used with things that are not enums). So unapply might not be needed if proposal-pattern-matching supports view patterns or there is a follow-on proposal for view patterns.

mheiber avatar Nov 03 '20 18:11 mheiber

I wrote up a sketch for possible ADT support a few months ago, though the current struct proposal doesn't quite align with that design anymore: https://gist.github.com/rbuckton/4a5108fab40ac90551bf82d9884711b5

I also just wrote up this as another example of how this could work using normal classes/functions: https://gist.github.com/rbuckton/192c2922650e05a1ca9cd7c01be7fc6c

rbuckton avatar Nov 17 '21 02:11 rbuckton

@rbuckton I was thinking we could leverage something similar to Scala's Extractor pattern (i.e. give enum values an unapply method that returns a tuple containing the original inputs). That would enable extraction of values in the absence of pattern matching, and could hypothetically be worked into any pattern matching implementation.

Good news, https://github.com/tc39/proposal-extractors reached Stage 1 at the last TC39 meeting, so hopefully we will be able to build that up and build on it for ADT enums.

rbuckton avatar Oct 17 '22 21:10 rbuckton