-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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 Arrays #793
Comments
I use enum arrays frequently in C++. Here are some examples: The silly hacks (like |
Would we have a
|
This would be a nice addition for me. I used this approach quite frequently for a non-dynamically allocated game which relied heavily on tables for data and looking up by id. I actually found that when trying to represent similar ideas in rust using enums it was pretty rough going and I switched to just using integer values for the cost of some compile-time safety. Regarding iteration, would it suffice to just have the following:
This also keeps the type returned between normal arrays and enum arrays consistent ( I would expect you would far more often than not use the tag for indexing associated data. There is the argument that you would need to know the appropriate integer size to cast to but if you wanted to do math or other things on the index, I would think you would probably have a good idea on what to pick here. |
I would love this feature as well, but how restricted should this feature be? Is this allowed?
My guess is no, but what about a more reasonable example?
Here, the compiler would be able to give us an array of size 3, and when indexing, subtract with the 5 offset. EDIT: |
@Hejsil both examples would work, even without the memberIndex function. You'd have to specify the tag type though. The following examples include memberIndex and memberTag builtins only for illustrative purposes.
I'm not sure how I feel about it. Ideally, it would provide a third binding:
See also: #358 |
While we're at it, we should consider switch statements too. See also: #359
|
Well, as long as:
compiles down to the same as:
Then I'm happy with the feature. I was just pointing out, that the compiler has to add a hidden cost to enum arrays when the enum tags do not map to array index directly. |
As for the extra |
@Hejsil Since the index of the member can be different to the value in the enum, I'd expect that to compile to:
In your example, if you changed A to 10, you'd have an index out of bounds error, since the underlying array has length 3. You then wouldn't be able to refer to the first element of the array, since the array can only be indexed using an enum value, and it 'contains' an invalid index.
All this would happen at compile time, the enum tag's @memberIndex would map directly to the array index, and we'd also guarantee that every enum array access is valid. No runtime cost with added safety! It would be possible to do stuff like:
|
@raulgrell Wait, so enum arrays are only useful for compile-time known indexes? What about user input?
In this case, the compiler has to output code that can do the mapping from enum -> array index. |
I had only really considered compile-time use, but that's a really good point. Yeah, in this situation it's not really a zero-cost abstraction, but if you were using this kind of pattern without language support you'd have to do the mapping anyway. So yeah, either way I'm happy. |
Hmmm. Here is another question. Is there any value in having an enum array be ordered by tag declaration order?
If so, then indexing |
Also, maybe this feature requires special initialization syntax:
This ensures that if someone reorders members of |
In terms of ordering, that's not really an issue - I'm not sure that indexing [A]u8 is any different to indexing [B]u8. I'm still not sure we're on the same page. Could you make an example where the tag value has no relation to the indices, and the child type of the enum array is of a non-indexing type like f32?
This is a great suggestion. Somewhere in the documentation there is mention of both enums and packed enums.
|
Hmmmm. Just look up the I like the idea of compiler reordering of enums but not packed enums. It has this nice consistency with structs. That also gives the option for ordered vs unordered enum arrays, not that I have a real use case for ordered enum arrays. |
Actually, order matters if you intend to ever treat your enum array as a normal array.
This is useful for shared memory and stuff like that. |
fn memberIndex(comptime T: type, tag: T) usize {
const enum_info = @typeInfo(T);
const enum_fields = enum_info.Enum.fields;
inline for (enum_fields) |field, i| {
if (std.mem.eql(u8, field.name, @tagName(tag))) {
return i;
}
}
unreachable;
}
test "memberIndex" {
const Value = enum(u2) {
Zero,
One,
Two,
Three,
};
std.debug.warn("\n{}\n", memberIndex(Value, .Three));
std.debug.warn("{}\n", memberIndex(Value, .One));
std.debug.warn("{}\n", memberIndex(Value, .Two));
}
Output:
|
EnumArray: fn EnumArray(comptime T: type, comptime U: type) type {
return struct {
data: [@memberCount(T)]U,
fn get(self: *const @This(), tag: T) U {
return self.data[memberIndex(T, tag)];
}
fn set(self: *@This(), tag: T, value: U) void {
self.data[memberIndex(T, tag)] = value;
}
};
}
test "EnumArray" {
const Axis = enum {
X,
Y,
Z,
};
var vec3 = EnumArray(Axis, f32){ .data = undefined };
vec3.set(.X, 0.54);
vec3.set(.Y, -0.21);
vec3.set(.Z, 0.55);
std.debug.warn("\n{}\n", vec3.get(.X));
std.debug.warn("{}\n", vec3.get(.Y));
std.debug.warn("{}\n", vec3.get(.Z));
}
Note that a branching penalty in |
I think it might be reasonable to restrict this proposal to untagged enums, or tagged enums that don't override any ordinal values. The use case for an enum array is (as far as I understand), to give names to the indexes of an array. The use case for specifying the ordinals of an enum is giving names to specific ordinal values. If those two cases ever overlap, you could actually use a constant enum array to look up the custom ordinal value. For example: const E = enum {
hundred,
thousand,
million,
};
const ord_E = [Value]u32{
hundred = 100,
thousand = 1000,
million = 1000000,
};
test "access enum ordinal value" {
try expect(ord_E[.hundred] == 100);
try expect(ord_E[.thousand] == 1000);
try expect(ord_E[.million] == 1000000);
} This is slightly more verbose, but I don't think that's necessarily a bad thing in this case. Using an enum with overridden ordinal values implies more overhead, and this makes that overhead explicit. In addition, the extra verbosity is only when defining the enum, because accessing |
Considering the discussion on enum slices, I'm not sure they're a good idea. Enum tags do not necessarily have a strong ordering, so ranges between them could be confusing and fragile. If you have an enum called For similar reasons, I think it's a good idea to require struct initialisation syntax for enum arrays. |
@MageJohn After reading your suggestions, a question occurs to me: would it be better to treat this concept as a struct with uniform field types, as opposed to an array? Because thinking about it, if all an Enum-Indexed array would do is give names to offsets in a series of values, that's pretty much what structs are for, with the differences being:
Though, these appear to be relatively trivial to resolve, compared to figuring out what should and shouldn't be allowed in Enum-Indexed arrays, imo. One approach for the first issue would be introducing something like I don't want to derail discussion here too much, in case this idea should be considered separately, or if it's already been considered, so I'll leave it at that, but it's something to consider. |
@InKryption I think that's worth discussing, yeah. Implemented correctly, it could neatly sidestep some of the issues discussed so far by reframing the concept. I think there are a few problems though, which I can't see easy solutions for. At the conceptual level, I'm not sure it maps as neatly onto existing concepts as an enum array. In my mental model the fundamental difference between a struct and an array is that an array is uniform while a struct isn't; this model breaks in the face of There are also questions of access syntax. An use case of tagged structs/enum arrays over structs would be access by runtime known values, which doesn't really work with dot access (e.g. |
One use case for an enum array is to comptime-compute maximum buffers size and use the enum to point to offsets into the buffer for returning the according slice or (better?) generate at comptime an array. What I think is missing in the proposal is to clarify if the enum If the data itself is contiguous and the backed integer is not necessary (as space waste), then one should be able to implement the comptime-computation of the slice, since one can generalize over the enum fields at comptime. Can you describe your use cases for non-contiguous data? Sketch of implementation with current capabilities of the language: comptime construction input:
output:
As of now, there is currently a miscompilation, which prevents things from working smoothly: https://gist.github.com/matu3ba/07ea081b07639e7ecceb13173083484f posted in this issue #10920 |
Isn't this already done? https://ziglang.org/documentation/master/std/#root;enums.EnumArray |
@Pyrolistical So far |
Indexing by a (simple) enum would be nice. I always loved that in Delphi. It can make code very clear. |
This was originally proposed by @raulgrell here: #770 (comment)
What do you think of an enum array? It has a length equal to the member count of the enum and can only be indexed with an enum value.
You can get close to this with status quo Zig by specifying the tag type and casting
But then if you change the number of elements, the backing type of the enum or override the values, you need to change a lot of code. And you could still access it with arbitrary integers, so if at any point the index into the array was hardcoded, it would have to be found.
An enum array basically becomes a comptime-checked map!
It could be approximately implemented in userland with something like this if we had a memberIndex built-in or something:
The text was updated successfully, but these errors were encountered: