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

Alternative enum proposal #1

Open
rwaldron opened this issue May 12, 2018 · 15 comments
Open

Alternative enum proposal #1

rwaldron opened this issue May 12, 2018 · 15 comments

Comments

@rwaldron
Copy link

I've been waiting on feedback from @bterlson on an enum proposal that I asked for review on about 5 months ago. I was last under the impression that he wanted to share that with you before responding, and I've been self imposing a block on it until I got that review back. I understand that people are busy and this work isn't everyone's priority, but I've put a substantial amount of time and consideration into that proposal and I think it has some interesting ideas that are worth pursuing. If you're interested in combining the proposal, I'd be up for that, otherwise I will push forward independent of this proposal.

@rbuckton
Copy link
Owner

@rwaldron I also have been discussing this with @bterlson for a few months now, but I don't recall any mention of this. Is there anything you can share that I can review?

@bterlson
Copy link

@rwaldron I am sorry, you should know I haven't given Ron much feedback on his proposal either. As most of my job is connecting disparate work streams together, I consider this a personal failure. Thanks for understanding.

FWIW, the link to Rick's proposal is here: https://github.com/rwaldron/proposal-enum-definitions.

@rbuckton
Copy link
Owner

rbuckton commented May 14, 2018

@rwaldron, I've taken a look and here are some of my thoughts:

  • enum is a new LexicalDeclaration binding form, similar to let or const.

In general I agree with the semantics (block-scoped, no use before def, TDZ) but I don't necessarily believe this would be nested under LexicalDeclaration as I'd like to be able to decorate it in the long term. I'd liken it more like a ClassDeclaration, and would eventually like to support decorators.

  • typeof enum is object

In my proposal, given enum E { A, B }:

  • typeof E is "function", to allow coercion
  • typeof E.A is enum, and I'll get into why in a moment.
  • EnumDeclaration with no BindingIdentifier creates const bindings corresponding to each EnumEntryName.

This seems like it would conflict or be confusing with using an enum as the default export.

  • EnumDeclaration or EnumExpression with a _BindingIdentifier creates:
    • a proto-less, frozen object;

This is doable, by moving much of the logic from %Enum% in my proposal to Enum, but seems a bit less convenient for coercion.

Also, I initially had an EnumExpression in this proposal but was advised to remove it for now to simplify the Stage 0 explanation.

  • EnumDeclaration or EnumExpression will set the value of each entry to an integer, starting at 0 and incrementing by 1 for each entry.

This behavior is roughly analogous to my proposal, minus the typeof difference.

  • EnumDeclaration may have EnumEntryName inline assignment overrides...

This behavior is generally identical.

  • EnumDeclaration may have ComputedPropertyName as EnumEntryName.

I also had this originally but removed it to simplify the Stage 0 explanation.

  • EnumDeclaration may not have duplicate EnumEntryName.

I concur with these semantics, but left it out of the Stage 0 explanation.

  • EnumDeclaration with an _EnumValueMap to populate the values of the Enum:

This is something I would rather see done with a decorator (which I also want to support but have left out of the Stage 0 explanation).

As why typeof E.A is "enum", its actually something I'd rather not do (as I would generally prefer the simpler semantics of just using Number, String, etc.), however I've chosen this approach as a compromise between two different groups:

  1. Those that want the value of each enum member to be a Number by default.
  2. Those that want the value of each enum member to be a Symbol by default.

It might be the case that we could argue for (1), with a built in decorator to achieve (2), which would greatly simplify this proposal:

@Enum.asSymbols
enum Colors { Red, Yellow, Green, Blue }
typeof Colors.Red === "symbol"
Colors.Red.toString() === "Symbol(Colors.Red)"

@ljharb
Copy link

ljharb commented May 14, 2018

what about:

enum(Number) Colors { Red: 255, Yellow: 255, Green: 255, Blue: 255 }
typeof Colors.Red === 'number' // Number(255)
enum(String) Colors { Red, Yellow, Green, Blue }
typeof Colors.Red === 'string' // String('Red')
enum(Symbol) Colors { Red, Yellow, Green, Blue }
typeof Colors.Red === 'symbol' // Symbol('Red')
enum(BigInt) Colors { Red: 255, Yellow: 255, Green: 255, Blue: 255 }
typeof Colors.Red === 'bigint' // BigInt(255)

or something similar?

@rbuckton
Copy link
Owner

You would need much more complex syntax to determine how you perform your mapping. If its just calling your EnumValueMap function with the SV of the name and the value, then your examples for Number and BigInt don't work as they would get NaN (i.e. Number("Red", 255)) and whatever an invalid parse of "Red" is for BigInt would be (i.e. BigInt("Red", 255)), respectively.

One of the reasons I like the decorator approach is that it is much more flexible:

@Enum.numbers enum Colors { Red = 0xff0000, Green = 0x00ff00, Blue = 0x0000ff } 
typeof Colors.Red === "number"

@Enum.strings enum Colors { Red, Green, Blue }
typeof Colors.Red === "String"

...

@warnExceedSMI32 // user-supplied decorator
@Enum.flags
enum SymbolFlags {
  None, // 0
  FunctionScopedVariable, // 0x1
  BlockScopedVariable, // 0x2
  Property, // 0x4
  // etc.
}

An alternative that I've considered might be extends, i.e.:

enum Colors extends Number { ... }
enum Colors extends String { ... }

With a restricted set of allowed values in extends (i.e. must be %Number%, %String%, %Symbol%, etc.)where we could chose the appropriate default mapping on a case-by-case basis, and turn off auto-increment for non-number supertypes.

I'd still probably want to use a decorator to apply mappings for things like bitwise flag values though.

@ljharb
Copy link

ljharb commented May 14, 2018

i like extends there; I'd want to not have to use decorators to make enum types useful (and i think having typeof 3 be enum would be super weird - specifically, having typeof Enum.foo !== typeof (0, Enum.foo) would seem like a nonstarter.

@rbuckton
Copy link
Owner

Also, the extends syntax is somewhat similar to C# enums:

enum Colors: int { ... }
enum Colors: ushort { ... }

@rbuckton
Copy link
Owner

With the current proposal, the result of typeof (0, Enum.foo) would still be "enum". Its not until you do something like typeof Enum.foo !== typeof (Enum.foo + 0) where it breaks down.

@ljharb
Copy link

ljharb commented May 14, 2018

When Enum.foo is a string but typeof enum, what would String.prototype.valueOf.call(Enum.foo) do? (i'd expect either Enum.foo is an object boxing a string, or else it'd have to throw)

@rbuckton
Copy link
Owner

@rwaldron, @ljharb, @bterlson: What do you think about the changes in #2?

@rbuckton
Copy link
Owner

When Enum.foo is a string but typeof enum, what would String.prototype.valueOf.call(Enum.foo) do? (i'd expect either Enum.foo is an object boxing a string, or else it'd have to throw)

@ljharb I don't think that relates to this issue. Would you mind adding a different issue for this?

@ljharb
Copy link

ljharb commented May 15, 2018

Filed as #3.

@rwaldron
Copy link
Author

rwaldron commented May 15, 2018

@rbuckton thanks for the reply. To be clear, the design I have is the result of years of iteration, consideration and revision. The latest design document was published in November.

@doug-wade
Copy link

I've been working on one as well, and was just about to start the legwork when I found this proposal. Not sure what's work taking, but here's some links in case:

@dead-claudia
Copy link

@rbuckton I have been working on one myself, too. The Git history is slim over there, but I've been trying various ways to see how best to fit enums into JS for a few years now. I'm a very heavy enum and bit mask user personally, and so I've gone through several different ways before finally, only recently, solidifying on any sort of pattern.

  • First, I tried just doing a bunch of string literals. Of course, stringly-typed programs are a bit hard to maintain, and you have to go constantly rewriting code in various places.
  • Then, I moved them to named values. This solved the issue of discerning names, so it made the representation easier to abstract from the use site itself. However, I didn't have any real pattern to this other than grouping, so adding new values became a little cumbersome.
  • Then, I started toying with object literals. Around this time, I also started using numbers instead of strings for discriminants. As I got deeper into this, I started running into issues with variants' values depending on others' values, and for obvious reasons, this is not exactly easy.
  • Then, I went back to constants again, but grouped with a prefix by convention. This provides many of the same benefits as object literals (specifically, being obviously a variant), while not losing the ability to depend on other members of the same "enum". But when defining the enum, this is a bit uncomfortably boilerplatey for me. This is approximately where I am now.

I also found myself slowly shifting how I was modeling related data with them:

  • First, I started with giant switch statements. That was pretty boilerplatey.
  • Then, I moved to object tables. This was easy to do when my discriminants were strings, as I could just do simple lookups. The problem is, I was still coupled to the string representation.
  • Then, I moved to Object.create(null) and just assigning dynamically. This decoupled the representation from the table. But for numeric indices, that's a bit wasteful.
  • Then, I moved to array literals. These are a bit more compact, so lookup tables using these with indices are much more efficient, and they're fairly workable when the enum variants can be made consecutive integers (read: 99.9% of the time for me). But you then lose the ability to monitor what value matches up with what variant, and it makes it easy to have subtle bugs over passing the wrong index or off-by-N errors if your data contains holes.
  • Then, I started using pre-allocated arrays and/or typed arrays and manually assigning the indices like I did with object literals. These share the same efficiency gains, but I can also get away with wasted space if my indices aren't 0 to whatever. Holes are also easier to specify: you just don't include them. But declaring them is a bit uncomfortably verbose for me, as you're repeating the same 4 tokens (Table [ $type ] = $value) + whitespace. This is approximately where I am now.

And I also discovered a few patterns along the way:

  • Most of my enums either have no associated data, just an index, or an index + some individual related value.
  • Most of my enum handling has either been one of three things: a non-trivial switch, something that can largely be done via a table lookup or three, or direct use and/or manipulation of their values.
  • It's not uncommon to want to serialize and/or deserialize enum variants. I personally have done this in several cases, and it comes up frequently when dealing with protocols and non-trivial IPC/worker communication. (I've written a few of those).
  • A large amount of my performance-sensitive code involves the use of enum-like structures and/or bit flags to some extent.

Initially, I was against enums, but I wanted to experiment, so I started looking at the issues I've had with the various formats:

  • String enums are easy to prototype, but they're slow, and nobody wants those for anything performance-sensitive. Instead of doing simple identity like with most things, you have to iterate the string to see if it's equal to the other. And unlike in, say, Lua or Python, you can't always just drop into C and sprinkle some magic fairy dust to get better performance in JS - WebAssembly only works for number crunching ATM, not much else, so that's out of the picture for now.
  • Numeric enums, and in particular integer enums, are amazing for performance, but a PITA to debug. It's fairly easy when you're debugging a few variants, and it gets a little challenging once you're at a dozen or so, but when you're debugging upwards of 30-40 variants, each representing a state (which I've had more than once), it starts feeling like debugging in GDB without the debugger symbols.
  • Symbols are globally unique and can carry helpful descriptions, but they are very awkward to (de)serialize. They are also pretty useless in anything number-heavy since you can't really reinterpret them, but that's probably incredibly obvious.

And that's why I decided to try to do a "best of all worlds" approach: I wanted to have my cake and eat it too. I wanted the uniqueness and of symbols for things like Flux/Redux reducers, the performance of numbers, the debuggability of strings, and a syntax that was cleaner than all three. In addition, I wanted to be able to specify data in a way that wasn't coupled to the enums themselves, something I already enjoyed being able to do with my style, but with some degree of verification that I didn't screw up and miss something on accident (which happens quite frequently with these kinds of tables when the names are unmarked).

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

No branches or pull requests

6 participants