wren icon indicating copy to clipboard operation
wren copied to clipboard

[RFC] Enums

Open clsource opened this issue 3 years ago • 5 comments

Idea for Enums in Wren:

Use #!enum attribute. This will tell the compiler that if there are no braces after an identifier, considers its value as the identifier name. All properties would be static.

#!enum
class CLanguageStandard {
    c89 // same as static c89 { "c89" }
    c90
    c99
    c11
    c17
    c18
    c2x
    gnu89
    gnu90
    gnu99
    gnu11
    gnu17
    gnu18
    gnu2x
    iso9899_1990 {"iso9899:1990"}
    iso9899_199409 {"iso9899:199409"}
    iso9899_1999 {"iso9899:1999"}
    iso9899_2011 {"iso9899:2011"}
    iso9899_2017 {"iso9899:2017"}
    iso9899_2018 {"iso9899:2018"}
}

clsource avatar Sep 04 '21 19:09 clsource

I'd love to explore something like this with a pre-processor. :-) This could be easily done WITHOUT support in core Wren.

joshgoebel avatar Sep 04 '21 22:09 joshgoebel

At present I create enums dynamically using Meta.compile. So I'd do something like this:

import "./dynamic" for Enum

var Direction = Enum.create("Direction", ["north", "south", "east", "west"])
var dir = Direction.east
System.print(dir) // 2

This works well enough but I think enums are important enough to be included in the language proper even if behind the scenes the compiler just converts them to a class with a bunch of static read-only properties.

Most languages seem to have them in some shape or form though I've always found Go's take on them very peculiar!

PureFox48 avatar Sep 08 '21 08:09 PureFox48

For incrementaly numeric values. If no braces are found after an identifier and a numeric value is found in the #!enum attribute. Take that as the starter value for a serial number.

#!enum = 1
class Fingers {
  // incrementaly store the values
  thumb // same as static thumb { 1 }
  index // same as static index { 2 }
  middle // same as static middle { 3 }
  ring // same as static ring { 4 }
  pinky // same as static pinky { 5 }
}

If a step is needed for each value, a group in enum can be used to specify it.

#!enum( 
   start = 0,
   step = 2)

clsource avatar Sep 09 '21 15:09 clsource

I have an option in my dynamic Enum to start from a different value than 0 though I didn't bother to add a step as I couldn't think of any obvious use cases.

However, I do have a Flags enum where successive values are powers of two, starting from 2^0 = 1.

PureFox48 avatar Sep 10 '21 16:09 PureFox48

The other option is create a class like https://github.com/spatie/enum and use the Meta way like PureFox48 described

clsource avatar Sep 19 '21 16:09 clsource

My vote would be a breaking change to introduce a new reserved keyword for "enum":

enum State {
    nothing                // default to 0
    active                 // default to 1
    inactive               // default to 2
    undetermined { 666 }
    error                  // 667
}

enum Math {
    pi { 3.141592 }
    e { 2.718281 }
    goldratio { 1.618033 }
}

enum Company {
    ceo { "Gauss" }
    cto { "Eurel" }
    cfo { "Nostradamus" }
}

enum Mixed {
    one { Hello World }
    two { 3.1415 }
    three { 666 }
    four { true }
}

mwasplund avatar Dec 24 '22 18:12 mwasplund

Matthew proposition is not realistic. It opens to much edge cases and requires the compiler to understand/remember enum values. What is the default value of an enum of String or any other type. If enums values are evaluated, there is a possibility that theses values are not constants. At minimum, the grammar should be at minimum changed to EnumEntry = ValueComputation, to convey the idea the value is computed only once.

mhermier avatar Dec 25 '22 10:12 mhermier

In this recent Hacker News item, there was some reasoned criticism of Wren on a couple of fronts.

The point about binary operators not being symmetric in Wren was discussed at length in #793 and is not an easy problem to solve.

However, I think we could address the absence of enums fairly easily as long as we keep it simple - just restrict it to consecutive integer values which probably covers at least 90% of use cases for enums in C.

For syntax I'd suggest

enum Direction(0) {  // the 'start' parameter could be any constant integer
    north
    east
    south
    west
}

On seeing that, the compiler would treat it is as though it had been written:

class Direction {
    static start { __start = 0 }
 
    static north { __start }
    static east  { __start + 1 }
    static south { __start + 2 }
    static west  { __start + 3 }
}

and generate bytecode accordingly.

The 'start' parameter would be optional and, if omitted, would be given a default value of 0.

We might need to think up a different name for 'start' so it wasn't likely to clash with the names chosen for the enum constants themselves.

All the other 'enum' cases envisioned by @mwasplund could be dealt with almost as easily by manually expressing them as classes with read-only static properties,

PureFox48 avatar Dec 25 '22 12:12 PureFox48

Matthew proposition is not realistic. It opens to much edge cases and requires the compiler to understand/remember enum values. What is the default value of an enum of String or any other type. If enums values are evaluated, there is a possibility that theses values are not constants. At minimum, the grammar should be at minimum changed to EnumEntry = ValueComputation, to convey the idea the value is computed only once.

My examples were meant to be very broad. At its core I was calling out the enum keyword as opposed to a shebang mutation of existing class definitions. I am fine if the underlying type has to be an integer to simplify the implementation. Could you not require that the enum value getter with initial values was implemented using a constant value? After that it seems like my sample could be syntactic sugar for a constant integer value in the bytecode generated. But I defer to the language experts.

mwasplund avatar Dec 25 '22 16:12 mwasplund

It all depends on the flexibility and constraints you are willing to impose on your enum implementation. The compiler is a dumb simple pass compiler, so what you are referring as no real meaning in our case. We can restrict enums to be integer only (though there are no such thing as integer in the VM). But people raised that it should be more general more like a named constant dictionary. We may need to allow to provide a default value generator, and possibly other fancy stuff like String conversions. So it is not as easy as do what the others do, though we could restrict ourselves to do so.

mhermier avatar Dec 25 '22 17:12 mhermier

As XMAS TV is crap as usual, I've just written some code to generate a group of related constants of any type dynamically and illustrated how this could be used for @mwasplund's examples.

import "meta" for Meta

/* Group creates a group of related named constants of any type.
   The group has static property getters for each member.
*/
class Group {
    // Creates a class for the Group (with an underscore after the name) and
    // returns a reference to it.
    static create(name, members, values) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (members.isEmpty) Fiber.abort("A group must have at least one member.")
        if (members.count != values.count) Fiber.abort("There must be as many values as members.")
        name = name +  "_"
        var s = "class %(name) {\n"
        for (i in 0...members.count) {
            var m = members[i]
            if (values[i] is String) {
                s = s + "    static %(m) { \"%(values[i])\" }\n"
            } else {
                s = s + "    static %(m) { %(values[i]) }\n"
            }
        }
        s = s + "}\n"
        s = s + "return %(name)"
        return Meta.compile(s).call()
     }
}
var State = Group.create("State", ["nothing", "active", "inactive", "undetermined", "error"],
[0, 1, 2, 666, 667])
System.print(State.error) //> 667

var Math = Group.create("Math", ["pi", "e", "goldratio"], [3.141592, 2.718281, 1.618033])
System.print(Math.e)      //> 2.718281

var Company = Group.create("Company", ["ceo", "cto", "cfo"], ["Gauss", "Eurel", "Nostradamus"])
System.print(Company.ceo) //> Gauss

var Mixed = Group.create("Mixed", ["one", "two", "three", "four"], ["Hello World", 3.1415, 666, true])
System.print(Mixed.one)   //> Hello World
System.print(Mixed.four)  //> true

This, of course, is different from just using a map as the resulting class is immutable and the property values are as near to constants as Wren can get.

Although C-style enums can also be generated dynamically, I still think it would be worthwhile for these to be included in the language proper.

PureFox48 avatar Dec 25 '22 17:12 PureFox48