VL-Language icon indicating copy to clipboard operation
VL-Language copied to clipboard

[Quest] Enums as first class citizens

Open sebllll opened this issue 4 years ago • 9 comments

At the time of this writing, one has to define Enums in c# and import them. But enums are nice and can enhance the understanding of patches a lot compared to bools or other constructs.

An additional question that came to my mid is if it would be nice to have the int values for enums defined manually or automatically, so that one can use bitwise operations on them later? those bitwise operations would then maybe need another nodeset/syntactic-sugar.

like so:

private enum Targets : uint
{
    None = 0,
    foo= 1,
    bar= 2,
    foobar= 4,
    anotherfoo= 8,
    anotherbar= 16,
    anotherfoobar= 32,
    All = uint.MaxValue
}

that said, i'm not sure about the (u)int stuff at all

sebllll avatar Jun 18 '20 13:06 sebllll

Yeah, it's a shame to have to go to C# to express something that simple.

I am unsure about the idea to limit the number of possible entries to 64 + None + All. On the other hand, it sounds like a lot...

Let us contrast this idea to how C# interprets an enum definition with

  • unspecified integer values for enum entries
  • unspecified backing type

It magically does this:

private enum Targets : int
{
    foo= 0,
    bar= 1,
    foobar= 2,
    anotherfoo= 3,
    anotherbar= 4,
    anotherfoobar= 5
}

https://docs.microsoft.com/de-de/dotnet/csharp/language-reference/builtin-types/enum

I am totally fine with doing stuff differently if we have a good reason. Maybe it is enough to change the entry values, but stick with the int enum type? I don't have a particular example where the different backing types would be problem though.

Having Nodes (or entries) like All and None sounds nice as well. However, All might be better defined as foo | bar | ... for that particular type.

We probably should put a [Flags] Attribute on top, which affects the text rendering (object.ToString()).

Related: #13

gregsn avatar Jun 18 '20 17:06 gregsn

We even could switch to the 0, 1, 2 encoding in case there are too many entries. In this case, you suddenly lose the |, the &, and the ^. Magic flip You typically should anyways serialize the string representation, not the integer value. But you can't know what a user does with your enum. So being able to define the enum entries and to be able to keep them at stable values is probably wanted.

So how about three modes:

  • Sequential: C# style
  • Flags: like specified above; you get an error if there are too many entries.
  • Manual: allows tweaking each enum entry value and backing type

No magic flip involved. Now we can try to figure out what default is best. Ja, why not Flags?

gregsn avatar Jun 18 '20 19:06 gregsn

I also took another read on this topic and found this thread quite helpful: https://stackoverflow.com/questions/7129976/flag-enums-without-power-of-two-values

so, my assumption, that |, & an ^ only work when the values are powerOfTwo seems right.

and there's also this case, where combinations are quite nice:

[Flags]
public enum FilePermissions
{
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,

    ReadWrite = 3, // Read | Write,
    ReadWriteExecute = 7 // Read | Write | Execute
}

(i'm not sure if one even has to define the last 2 entries in the enum, since they can be inferred implicit)

i also like the auto switch, which is the combination of your proposed Sequential and Flags:

  • use Flags as long thhere are less that 64 entries
  • switch to sequential if there's more
  • issue a Warning in that case

wouldn't that be a nice default with all the possible benefits (until more than 64)?


on the other hand, when reading those Guidelines for Flags/Enums, i could also think of 2 separate things: Flag and Enum. And then let the user decide if bitwise ops are wanted and choose accordingly. the system would then allow the bitwise operators only for Flags.

that clear distinction would be even more consistent i'd say. and the end-user story is also super simple:

  • there's Enum which is a list of named entries where you can choose from.
  • if you want to have bitwise operations for such a thing, which might make sense in some cases,you can use Flag. This is an advanced Enum that comes with extra Nodes |, & an ^, but its entries can't exceed 64

sebllll avatar Jun 18 '20 19:06 sebllll

(i'm not sure if one even has to define the last 2 entries in the enum, since they can be inferred implicit)

if you define them you get a decent text representation of values 3 and 7. 5 is still Read | Execute, but 3 is just ReadWrite.

(I don't know exactly how it works. I guess sometimes there are different ways to represent one value. If you wouldn't have defined ReadWriteExecute, but instead ReadExecute=5, 7 could be rendered ReadWrite | Execute or ReadExecute | Write (...))

Anyways. It's sometimes handy to define shortcuts for combinations. And with them suddenly ReadWrite | Execute makes sense (the | node makes sense), even for values that are not powers of two.


regarding calling it Enum or Flag: yes the end-user story. There are always many ways to sell it in the end. But part of it is UI in a visual language. I could imagine that the model (the source code) underneath should probably be a new Enum element, with a mode property that could have a value of:

  • Sequential: C# style
  • Flags: As specified above; you get an error if there are too many entries.
  • Manual: allows tweaking each enum entry value and backing type
  • MagicFlip (since you brought it up again)

gregsn avatar Jun 18 '20 22:06 gregsn

What about

  • Sequential: C# default style
  • Flags: define backing type (ushort, uint or ulong), entry value pow2 incremented
  • Explicit: define backing type and each entry value ...

Flags with max 64 entries sounds like defaulting to ulong. Thinking serialization and networking that would be pretty big. Also interfacing with (lowlevel) APIs could be handled very direct by just specifying the underlying bit/bytewidth (e.g. win.h with all those DWORD flags, Firmata/MIDI,...)

For library implementers it would probably be nice to use flags but expose them as simple enums to the user without redefining the type. Wrapping an API one often encounters non sequential flags and one might want to avoid having a library user e.g. sending a wrongly shifted bit to some piece of hardware.

woeishi avatar Jun 19 '20 01:06 woeishi

  • Sequential, Flags, Explicit. explicit sounds nice as well.
  • Flags backing type: are you saying that you want to specify the backing type manually here as well? what would be the default? would you only allow unsigned types (like Byte, UInt16, UInt32, UInt64)? UInt32 sounds like a good default to me.

however we should again check how the user typically would cast back and forth when working with ordinal values. I think that a hard cast results in a runtime exception if you accidentally used UInt32 instead of Int32.

Flags with max 64 entries sounds like defaulting to ulong

oops. sorry. no, let's go for 32.

For library implementers it would probably be nice to use flags but expose them as simple enums to the user without redefining the type. Wrapping an API one often encounters non sequential flags and one might want to avoid having a library user e.g. sending a wrongly shifted bit to some piece of hardware.

can you rephrase that? what's the suggestion here? getting rid of some entries or the possibility to work on a bit level? Maybe you just want to have |, &, ^ being internal? That we probably could do via some checkbox... Yes.

gregsn avatar Jun 22 '20 13:06 gregsn

default underlying type of flags: i guess it would make sense to follow c#. there are definitely flags with backing signed types around. so restricting to unsigned might be too harsh.

By default, the associated constant values of enum members are of type int; they start with zero and increase by one following the definition text order. You can explicitly specify any other integral numeric type as an underlying type of an enumeration type.

via msdn

can you rephrase that? what's the suggestion here? getting rid of some entries or the possibility to work on a bit level?

Maybe you just want to have |, &, ^ being internal? That we probably could do via some checkbox... Yes. internal yes! a checkbox sounds nice, to hide the bit ops for the public facing side.

woeishi avatar Jun 23 '20 13:06 woeishi

hm, i use the bitwise operations quite often in application patches. what is the rationale behind hiding those?

and also: this quest is not only about importing flags/enums but also about providing a possiblility for the users to define them. wouldn't it feel strange, when you for example have a MyEnums.vl referenced in your MyProject.vl and then can't access bit ops from inside MyProject.vl?

sebllll avatar Jun 23 '20 16:06 sebllll

it's just about an option to be able to hide them. not by default.

usecase would be: LibWrapper.vl nicely wrapping functions like genericCall(payload, combinationOfFlags) for the lib user as specificCallA(payload, simpleEnums), specificCallB(payload, simpleEnums),... which in cases can be less error prone especially if flags are not sequential and using them without detailed knowledge which enum flags of the lib can be validly combined can cause havoc. The lib implementor could of course implement 'normal' enus on the user facing side and use flags internally. that would require loads of casts though. that's the reason i was asking, if a simple ui option to mark them internal was possible.

woeishi avatar Jun 24 '20 14:06 woeishi