-
-
Notifications
You must be signed in to change notification settings - Fork 163
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
enum with values / distinct const #1129
Comments
Just some thoughts. Would there be auto-incrementing like C enums? With |
Auto-incrementation could happen, although I don't see all that much use for it to be honest. When you're using values that's usually what you want for everything, barring the case where you're reserving It could be possible that you could get a list of all the constants in some way using reflection, but there are nuances here. It's obviously not possible to do a name <=> value mapping, so at compile time, what is passed around if we get a compile time reference to such a thing? Like for enums we could provide a list of all the names and a list for all the values. Such a thing could exist both at compile time and runtime without any problems as long as it isn't extensible (i.e. possible to add more constants at a later time) One thing to note is how such compile time capabilities interact with the forward/backwards compatibility of enums. It is common in frameworks to see the equivalent of this: // Framework 5.0
enum Foo
{
FOO_ONE = 3,
FOO_TWO = 100,
}
// Framework 6.0
enum Foo
{
FOO_ONE = 3,
FOO_TEST = 100,
FOO_TWO = FOO_TEST @deprecated("use FOO_TEST"),
FOO_HELLO = -10239
} If we think about how this interacts with an application that bound to the 5.0 framework. Foo f = get_foo_from_system();
String[] all_foos = Foo.names(); // Compile time generated from 5.0
int[] all_foo_values = Foo.values(); // Compile time generated from 5.0
foreach (i, val : all_foo_values)
{
if (val == f) return all_foos[i];
}
return "Invalid foo" So some care has to be taken as to what the guarantees are and so on. This is similar for enums, but may be more surprising for a user in this case. Regarding name collisions The argument against inference for |
Yeah, good points. Now I'm questioning what else reflection would even be useful for. |
I think it's mostly automating debug things. |
Possibly some mapping. |
This is almost the most awaited feature for me, thank you! :)
In addition to point 1: what if ordinal enum becomes the default enum and current enum will be renamed to |
I tentatively tried implementing this, but as far as I can tell, the benefit seems small over enums with associated values: constset Foo : int
{
ABC = 3,
BCE = 123
}
enum Bar : int(int val)
{
ABC(3),
BCE(123)
}
// With constset
extern fn void some_c_foo(Foo f);
// With enum + associated value
extern fn void _some_c_bar(int val) @extern(some_c_bar) @local;
macro void some_c_bar(Bar bar) => _some_c_bar(bar.val); |
The advantage is somewhat greater when going the other direction: // With constset
extern fn Foo c_foo();
...
switch (c_foo())
{
case Foo.ABC: ...
case Foo.BCE: ...
...
}
// With enum + associated value
extern fn int c_bar();
...
switch (c_bar())
{
case Bar.ABC.val: ...
case Bar.BCE.val: ...
} |
We could imagine supercharging associated values, in which case we'd get this: enum Bar : int(inline int val)
{
ABC(3),
BCE(123)
}
extern fn void some_c_bar(int val);
...
some_c_bar(Bar.ABC); However this loses some type safety. Which might be acceptable though. |
But we could go further with this, although at a greater cost to the grammar: // Imagine the associated type can be used as a pseudo-type:
extern fn void some_c_bar(Bar.val val);
...
some_c_bar(ABC); This is much more complex to handle, but still somewhat possible. If we think about it from an "ease of learning", I think this might be going overboard too. Unless we have usecases that are not when interfacing with C. |
It's fairly straightforward to construct a macro extern fn int _c_bar() @extern("c_bar") @local;
macro Bar c_bar() => @enum_by_value(Bar, val, c_bar()); Which then would convert an int to |
To add a bit to this, why do I say that it's "complex"? Well, for any variant of this that is anything but a convenient way of creating a distinct type + a list of constants for this type we require a new kind of type. This type must be checked for correct semantics everywhere, which means it adds to the general complexity of the semantic checker in a negative way. If it's merely a shorthand for: module baz;
distinct Foo = inline int;
Foo ABC = 3;
Foo BCE = 123; Then we're fine, because no extra type is needed, but then we don't get Foo.ABC or Foo::ABC either! This is why I bring up the associated values for enums. They are incredibly useful in some cases, but largely they seem overlooked. While they could be removed from the language, there isn't actually that much extra complexity in supporting the feature itself. So if this feature could accommodate the "distinct const" use-case as well, that would be a double win. I've already outlined some way it could be done, but to add one more: enum Bar : int(inline int val)
{
ABC(3),
BCE(123)
}
extern fn void some_c_bar(inline Bar b);
...
some_c_bar(ABC); Now the way this will work from the language's perspective is this:
The somewhat confusing result though is that the type of Rather than using extern fn void some_c_bar(Bar b @access(val)); Such a feature could in that case be more generally applied, and possible to use with any type. It is less obvious though. Without attribute it could look something like: extern fn void some_c_bar(Bar.val b); although this can certainly run into some grammar difficulties. It should also be noted that something like // This macro would then work on any enum with an inline parameter
macro void abc(inline b)
{ ... }
// This macro could extract "val" from any type:
macro void abc(b @access(val))
{ ... }
Another thing in favour of `@access` though is that we typically use these only for external functions:
```c
// Today
extern fn void _some_c_bar(int b) @inline @local;
fn void some_c_bar(Bar b) => some_c_bar(b.val);
// Access idea
extern fn void some_c_bar(Bar b @access(val)); |
Even though this: module baz;
distinct Foo = inline int;
const Foo ABC = 3;
const Foo BCE = 123; is technically a solution, it has the following issues:
So to me this is a granularity/module fragmentation issue. I'd rather have Instead what I'm doing right now is just prefix the names as you would do in C: distinct Foo = inline int;
const Foo FOO_ABC = 3;
const Foo FOO_BCE = 123; Which has the (minor) problem that you still have to qualify with at least one module when using the constants from another module so the names get even longer. distinct const Foo : inline int
{
ABC = 3,
BCE = 123
} for shortening names and usage to |
The enums with associated values is interesting as well, but it looks less intuitive, and it's usefulness seems to be limited to C interop. It's potentially more error prone too, since you might be tempted to cast the enum to int to get its real value instead of using |
Unfortunately Foo::ABC and Foo.ABC both require modeling it as a separate type. |
@cbuttner Associated values as such has been in the language from the beginning and is very useful for certain use-cases. So the problem here is more whether we can leverage them. I think the question you point out is valid: what happens with In that case there is no confusion as to what casts means, since they aren't used. |
Benefit №1: enum Foo : int
{
A = 3,
B, // B == 4 here
C = A + 10,
D = 1 << 4,
E = 1 << 5,
} Benefit №2: Also: What exactly the first
Previously, there were the usual normal enumerations. :) And sorry, I'm against new keywords or complicating syntax. |
enum Foo : int(int val)
{
A(3),
B(4),
C(A.val + 10),
D(1 << 4),
E(1 << 5),
}
When we're talking about the associated value yes. It is because the implementation is essentially one of: int[5] Foo_val = { 3, 4, 13, 16, 32 }; So that retrieving the mapping is one of indexing into this variable. If you do something like int x = Foo.A.val; Then this will actually automatically get folded into Foo a = get_foo();
int x = a.val;
The first int is the storage type of the enum.
Associated values were always there, they just costed more (a function call was needed) and had more problems (how do you get the name of an enum from a number when that number matches two values?) So even if we say "let's go back to C enums", that would also mean that everything you get for the enums today: And conversely, if we say "let's add C style enums too", they have exactly the problem of having to be yet another type to be handled in the type system. There is no free lunch here, just trade offs. |
Note here that we could add this: enum Foo : int(int val)
{
ABC = 1,
BCD = 3,
} As mere syntax sugar for enum Foo : int(int val)
{
ABC(1),
BCD(3),
} I would agree that the ABC(1) form leaves something to be desired visually, although it's fairly consistent. That said, this would be consistent as well: enum Foo : int { int val, double x }
{
ABC = { 1, 0.0 },
BCD = { 3, 0.4 },
} Such a form could perhaps more naturally have the shorthand enum Foo : int { int val }
{
ABC = 1,
BCD = 3,
} |
What about this: C-style enum: enum Foo : int
{
ABC = 1,
BCD = 3,
EFG
} Associated enum: enum Foo : ( int val, double x )
{
ABC { 1, 0.0 },
BCD { 3, 0.4 }
} |
As I said, keeping both C style enums and the regular ones is going to have a prohibitively high complexity cost, so this would need to be implemented as a type of enum with associated values, but where there is some convenience to let it be used similar to C enums. |
Ok. Let's remove associated enums completely. They can be replaced by structs: import std::io;
struct Value
{
int a;
double d;
}
struct Foo
{
Value a;
Value b;
}
fn void main()
{
const Foo FOO = { {1, 0}, {2, 2} };
io::printfn("FOO.a.a %s", FOO.a.a);
io::printfn("FOO.a.d %s", FOO.a.d);
io::printfn("FOO.b.a %s", FOO.b.a);
io::printfn("FOO.b.d %s", FOO.b.d);
} |
No they can't that doesn't make sense even. There is no "associated enum" either. There's just enums backed by ordinals and enums that are glorified constants i.e. C enums. The point is that ordinal enums support:
They are not getting removed, so please stop suggesting this over and over again. It just happens that associated values are easily implemented by ordinal based enums. For some reason you seem to insist that ordinal enums are like Swifts so-called "enums", which are actually just tagged unions but with broken naming. The ordinal enums have zero to do with tagged unions. Whether ordinal enums have associated values or not is entirely separate to whether enums should be ordinal based or just random constants. So again, let me state in no unclear terms that the ordinal enums are not being removed. Nor will they be considered a special case of enums. They are the normal form of enums and that is final. What this issue is discussing, is how to support "enums as a collection of random constants" which C also allows. From trying to implement it, it's 100% clear that anything beyond the "syntax sugar for constants" solution will require adding a new kind of type to the language. This is something that I found prohibitively costly. For that reason I am trying to find a way for "enum as a collection of random constants" to piggyback on ordinal enum. Using the associated values seems like an easy way to do this. |
enum Foo : int { int val }
{
ABC = { 1 },
BCD = { 3 },
} Is ambiguous, so some alternatives are enum Foo : { int val } int
{
ABC = { 1 },
BCD = { 3 },
}
enum Foo (int val) int
{
ABC = { 1 },
BCD = { 3 },
} Which have the advantage of allowing eliding the ordinal storage type, e.g. enum Foo : { int val }
{
ABC = { 1 },
BCD = { 3 },
} |
Here is a real example of an enum with associated value: enum Dir : char(int[<2>] dir)
{
NORTH({ 0, -1 }),
EAST ({ 1, 0 }),
SOUTH({ 0, 1 }),
WEST ({ -1, 0 }),
} We could imagine it being: enum Dir : { int[<2>] dir } char
{
NORTH = { 0, -1 },
EAST = { 1, 0 },
SOUTH = { 0, 1 },
WEST = { -1, 0 },
}
Dir x = ...
location += x.dir; |
Ah okay, I didn't know the cost was this high. So what's left is the original idea about the syntax sugar. That would still be an improvement since it makes a bundle of related constants more obvious to a reader, and external tools can parse it as a bundle too. I'm not against leveraging enums either. It's clear that runtime table lookups and accessing values with I'm not sure about the extern fn void some_c_bar(int val @from(Bar));
// Or
extern fn void some_c_bar(int foo @from(Bar.val)); Now that looks like a special case of parameter destructuring to me. Might still be too quirky though, I don't know, I leave that up to you. |
The feature is kind of hard to judge the scope of to be honest: regardless of what syntax is used, this is the kind of feature you then could leverage for other types as well. It's a very niche feature, but at the same time fairly complex in the possible uses. If we consider something like There's a reason for the "inline inline" feature: Substructs allows something like: struct Abc
{
inline Baz b;
int x;
} To be used both where If we just wanted The idea with extern fn void some_c_bar(inline Bar val); Was to then to essentially say that "you have to use a Bar, but it will actually pass on the inlined value of Bar, rather than the ordinal" |
Odin, Zig. |
Because you either have to have to different types under the same name, or you drop the functionality of the ordinal enums, such as runtime reflection. What language allows both and has name reflection like C3's for enums? How would that even work with enums-as-masks? It can't. |
Masks are useful in C, because C hasn't introspection. C3 has enumset in std. It's enough. |
Odin: keeps an array of name + value, taking the name at runtime means walking through the values and trying to match them. This doesn't work for masks or overlapping values. If Odin had associated values, it would similarly be a loop for every associated value to retrieve, that is O(n) rather than O(1) time. Which would make associated values expensive enough to never get used. Zig: creates a function with a switch, matching value to name. Again, doesn't work with masks. Overlapping values are forbidden for Zig enums. If they had associated values, those would be O(n). |
Interop with C is the only reason why non-ordinal enums in this issue is even brought up. |
After all this discussion, I can agree that non-ordinal enums are not stricly necessary. |
The code reading is what I'm concerned about myself, and I was kind of happy to have found a way to leverage associated values in a good way. However, in the end that feature increasingly seems like a bad idea and I will remove it. |
Ok, so another try, what if we do this: enum Foo : inline int(int val)
{
ABC = 1,
BDF = 2,
}
// For this enum, it will implicitly convert to
// the ordinal:
int a = Foo.ABC; // a = 0 We can also inline a parameter: enum Bar : int(inline int val)
{
ABC = 1,
BDF = 2,
}
// For this enum, it will implicitly convert to
// the ordinal:
int a = Bar.ABC; // a = 1 To avoid confusion, this will change the meaning of enum Baz { FOO, BAR };
int a = (int)Baz.FOO; // ERROR Instead it's required to use enum Baz { FOO, BAR };
int a = Baz.FOO.ordinal; // This is the way |
One further comment here is that this would strengthen the |
So how do we convert from an int to an enum? Again, we can't use the cast, that would lead to unwanted results, consider: int a = Bar.ABC; // a = 1
Bar b = (Bar)a; // b = Bar.BDF ! So we need to introduce a conversion function: Bar b = Bar.of_ordinal(1); // Bar.BDF This one can then also be typed with the index type (e.g if the ordinal type is ichar, then of_ordinal would take an ichar). |
So, depending where you put the But about your last comment, casting could lead to unwanted results if you inline What happens with repeated values? Are they valid? enum Bar : int(inline int val)
{
ABC = 1,
BDF = 2,
EHG = 2,
}
bool equal = Bar.BDF == Bar.EHG; // I would guess true.... Also, not only a Bar b = Bar.of_value(1); // It should be Bar.ABC
Bar c = Bar.of_value(2); // Bar.BDF or Bar.EHG ??? Any should be valid This is important because when reading protocols from the network, you may want to express the types with an enum. enum Bar : int(inline int val)
{
UNK = 0,
ABC = 1,
BDF = 2,
EHG = 2,
}
fn Bar getBarFromValue(int val) {
switch(val) {
case Bar.ABC: return Bar.ABC;
case Bar.BDF: return Bar.BDF;
default: return Bar.UNK;
}
} |
No, this is not true. Unconverted it is a comparison of ordinals. This would be true though:
I feel this has a very limited use. I don't mind writing a macro for it. It would need to look something like: |
Okay 👍
I was thinking more on a lookup table... but nvm. |
Note that enum Foo : (inline String val)
{
ABC = "Hello",
BCD = "World"
}
// ...
io::printfn("%s %s", (String)Foo.ABC, (String)Foo.BCD); What this means is that the comparison could be arbitrarily complex or even undefined. So it's not clear that it is suitable for a lookup table. |
Yeah, I still had in my mind that enum are for integers only, but this is much more powerful. Yeah, I think the current proposal could work fine for most cases. |
I want to highlight something however, this doesn't change the problem it's supposed to fix, namely C interfaces. // C interface
enum Foo { FOO_ABC = 123 };
void fooSomething(enum Foo); "Rejected" C interface: enum Foo : (int val) { ABC = 123 }
extern fn void fooSomething(int val);
// call
fooSomething(Foo.ABC.val); Other rejected C interface: distinct Foo = int;
const Foo FOO_ABC = 123;
extern fn void fooSomething(Foo val); The actual proposal enum Foo : (inline int val) { FOO_ABC = 123 }
extern fn void fooSomething(int val); // <- int, not Foo! We can improve it: distinct FooVal = int;
enum Foo : (inline FooVal val) { FOO_ABC = 123 }
extern fn void fooSomething(FooVal val); |
So really, we come back full circle and failed to do anything. Rather, it leads to reconsideration of:
constset Foo : int
{
ABC = 123,
}
extern fn void fooSomething(Foo val);
enum Foo : (int val) { ABC = 123 }
extern fn void fooSomething(Foo.val x);
// call
fooSomething(ABC); // implicit .val |
Other possible ways for expansion syntax: extern fn void fooSomething(Foo x @expand(x.val));
extern fn void fooSomething(Foo x.val);
extern fn void fooSomething(Foo x @expand(val);
extern fn void fooSomething(Foo x -> x.val);
extern fn void fooSomething(Foo x @extern(x.val));
extern fn void fooSomething(Foo x @export(x.val)); Of course there is the explicit macro wrapper too. |
There are probably other alternatives. |
A new type seems overkill. As this is something intended for C interop, Also, |
If it's used outside of C interfaces, that's probably bad, unless there is a good reason for it. |
If I understand correctly, the current implementation uses the type of the enum to store an identifier, and then it expands a virtual struct with the associated values. So this:
...acts similar (memory-wise) to something like this:
So to set values to enum members, the requirements would be:
So why not just make an attribute that simply makes an enum act like the ones in C?
...would fail, since
This would compile successfully, have the size of a
Tell me what you guys think. peace<3 |
Something like Channels x;
char y = x.size; Is really: char x;
char y = __Channels_size[x]; So then you understand that given that you have the size, to find the
|
Thanks for the explanation! And I agree with you on your takes, they're kinda obvious. But what your arguments are against is not what I wanted to describe. With
In memory, we'd have to retain a value for every entry up until 4. So in C++ terms: const int CHANNELS_COUNT = 5;
bool Channels[] = {
true,
false,
false,
true,
true
};
bool isPresent(char index) {
return (index < CHANNELS_COUNT) && Channels[index];
}
int main() {
isPresent(0); // true
isPresent(2); // false
isPresent(4); // true
isPresent(5); // false
// etc...
return 0;
} And to be less of a memory hog: const int CHANNELS_COUNT = 5;
long Channels = 0b10011;
bool isPresent(char index) {
return (index < CHANNELS_COUNT) && (Channels & (1 >> index));
}
// same tests and results as before ... Or if we want to retain identifier strings: const int CHANNELS_COUNT = 5;
void* Channels[] = {
(void*)"AUTO",
NULL,
NULL,
(void*)"RGB",
(void*)"RGBA"
};
bool isPresent(char index) {
return (index < CHANNELS_COUNT) && Channels[index];
}
int main() {
char index = 4;
bool result = isPresent(index);
if (result == true)
cout << (char*)Channels[index];
else
cout << "That's a miss";
return 0;
} Same thing as the first but now, a This way, the performance hit is kept pretty low, especially for desktop systems. Still, 2 or 3 times C, but eh. So this solution:
Thoughts? |
Did you think about the problem here that C enums also cover constants-that-are-bitmasks, and so to handle this yet another annotation is needed. |
Well, they would work, as the only requirement for my sketch is for the values to be unique, but yeah, it would be pretty inefficient... A new type seemed kinda overkill to me as well. (Sorry for the broken terminology btw.) |
There is the sub-module approach: module foo_bindings;
/* function and types here */
module foo_bindings::channels;
distinct Channels = int;
const Channels AUTO = 0;
const Channels RGB = 3;
const Channels RGBA = 4; Now we can use it in this manner: fn void main()
{
Channels channel = channels::RGB;
int a = 1 + (int)channel;
Channels rgba = (Channels)a;
assert(rgba == channels::RGBA)
} Here the module name becomes the effective namespace. |
If I may chime in here. I'm currently building a few C bindings with extensive use of defined values. And for that, I actually have several use cases for enums as a namespace with a mix of manually defined and auto-incrementing values. Just one example from GLFW that would be wonderful if it worked like this: enum KeyCode : CInt {
UNKNOWN = -1,
SPACE = 32,
APOSTROPHE = 39,
COMMA = 44,
MINUS, // auto-incremented to 45
PERIOD, // auto-incremented to 46
...
} The submodule approach is a usable workaround, but ends up being quite inconsistent when working with some values that fit an enum and others that don't. |
Enums changed with #428 from classic C enums to ordinal based enums.
This left a hole in the language, not the least in how to define C enums that aren't strict ordinals.
Early ideas included an attribute – which is bad because an attribute should be affecting the entire implementation of the enum. As well as just work around it with distinct type + sub module.
Later
distinct
was proposed for this, but worked poorly as it was not a keyword at the time. However, currently something likeCould be considered.
Questions remain in regards to semantic and usage. For example, is the usage:
baz::ABC
orFoo.ABC
orFoo::ABC
. The first case considers the code mere shorthand for:The second is close to enum style, but the question is then whether this is desirable giving the difference in semantics. The final variant
Foo::ABC
would give it a unique look, clearly indicating it is from a "const set", but then unlikeFoo.ABC
with the obvious inference of.ABC
it would not seem like it should implement inference.distinct const
while not requiring a new set of keywords is fairly long, and the question is whether this is good.Other syntactic alternatives would be:
All of those suppose a separate kind of types. The initial option with
baz::ABC
retains the option of just modelling this as constants.The text was updated successfully, but these errors were encountered: