-
-
Notifications
You must be signed in to change notification settings - Fork 21.3k
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
Add String-based Enum support to GDScript #20988
Comments
Yes. Right now I use dictionaries for this. |
Aren't dictionaries good enough? |
@hilfazer they're probably less efficient at lookups, and don't have IDE autocomplete support (one main advantage of enums for me, anyway). I'm unsure if a dictionary can be const, either, but I've never tried. The syntax is different than typical enum syntax, both in initialization and in lookup. |
@nobuyukinyuu @hilfazer Dictionaries can be const. The bigger problem is the fact that if what you want all along is simply strings and that's it (like in my use case, I just want to display categories in the editor and have no need for the integers since the strings get passed to a python script via OS.execute), then you end up having to use a separate data structure altogether (the enum) just to find the string you want, which leaves open the opportunity for errors in one's code via unintended index offsets or something. There's just no point to store one thing in two disjoint data structures if the one data structure is merely a means to an end. Especially when we can easily add string support with only a few lines of code, as evidenced in the PR I submitted. |
I also often wished I could have string enums and benefit from autocompletion + easy drop-down in the Inspector. For common things like a stateful object that lets the user pick a starting state, or giving a command option to some UI widget... I currently use |
IMHO, I would prefer that enums remain with integer values only. They are only sugar for constant dictionaries, and adding more features to them would just make for complicated sugar. That being said, if string support is implemented, there should probably be support for other types as well (Floats, Vectors, Arrays, etc.). At which point, it just becomes a glorified constant dictionary syntax, which doesn't use the The code example from the OP can be rewritten as: const Types = {
TYPE_FIRE = "Fire",
TYPE_ICE = "Ice",
}
# And, for quick access:
const TYPE_FIRE = Types.TYPE_FIRE
const TYPE_ICE = Types.TYPE_ICE But it should be more performant by just doing: enum Types {Fire, Ice} |
please show an example of how getting a value from an enum like dictionary is detrimental towards the game and is causing a bottleneck. |
@nobuyukinyuu @girng There's no difference in lookup time. You're either doing a property lookup when accessing the const value directly, or you're doing a const property lookup followed by a Dictionary lookup when going through the Dictionary/enum. The concern in the issue is merely to improve the usability so that string values can easily be associated with enum values (for printing purposes, passing to external code, whatever).
Except that allowing users to use strings doesn't "add more features" to them for more "complicated sugar" so much as it just takes away an arbitrary restriction on the permitted values. There's nothing stopping them from being strings except a single if statement that forces only INT variants through. All of the other logic in the related PR is just about circumventing the assumption that all values will be integers. Also, can't really do |
So, upon reconsidering this, I feel like there's a mis-solution I'm applying. I'm wanting to not have to generate the strings I want for an exported enum, but in MOST cases (although not my original case for creating this Issue), that's the only basis I would have for wanting string support. Implementation-wise, it's still ideal for the enum itself to use integer values and not support anything else. Integer comparisons are faster and take up less memory. Given this, I have another proposition: When annotations for GDScript are implemented (#20318), we could add an annotation for an enum that allows the user to specify an enum prefix (e.g. "TYPE_"). Then, when that enum is exported, the script would know to generate a list of strings by grabbing all of the string keys for the enum, stripping away the prefix, and converting it to title case. So...
I think this would be a much cleaner solution. |
Are there any other languages that do this? I'm not against this feature, just curious. |
Swift has beautiful enum system. It lets you do whacky and useful things like this:
They get around using pretty much any values by providing a |
That looks more like namespaced variables though. If anyone is going to put a proper 'enum' in the language then it should be a full (G)ADT type so it is actually more useful than just a basic 'dictionary'. I could imagine something like this: enum PhysicalAttack_Types:
Blunt
Sharp
Etc
enum MagicAttack_Types:
Fire
Nature
Etc
enum Attack_Types:
Unknown
Physical : PhysicalAttack_Types
Magic : MagicAttack_Types Used like (pretend that open PhysicalAttack_Types
open Attack_Types
attack = Physical(Sharp) An ADT is just an Enum that you can attack extra data to if a specific head/case so wishes. The type of all related heads can be defined at the call site as well. A GADT is just an ADT that can define the type of the 'enum'/ADT itself at the call site, this is important it the typed section of gdscript, less useful with the untyped. The traditional example is just a simple calculator, like you might do this with just normal ADT-style Enum's: enum Value:
Bool : bool
Int : int
enum Expr:
Value : Value
If : (Expr, Expr, Expr)
Eq : (Expr, Expr)
Lt : (Expr, Expr) So it's a value and an expression type, a simple interpreter type in other words, an AST. We can define such an ast like: open Value
open Expr
ast = If(LT(Value(Int(2)), Value(Int(4))), Value(Int(42)), Value(Int(0))) This is basically just def eval(expr): # `Expr -> Value` is the type of this function
match expr: # Just matching on the enum/adt head
case Value(v): v
case If(b, l, r):
match eval(b):
case Bool(true): eval(l)
case Bool(false): eval(r)
default: raise("Invalid AST")
case Lt(x, y):
match (eval(x), eval(y)):
case (Int(x), Int(y)): Bool(x<y)
default: raise("Invalid AST")
case Eq(a, b):
match (eval(a), eval(b)):
case (Int(x), Int(y)): Bool(x==y)
case (Bool(x), Bool(y)): Bool(x==y)
default: raise("Invalid AST") Notice how wordy it is, how verbose it is, how I'm doing something really stupid by encoding the validity of the AST into the runtime representation instead of making it impossible to construct an invalid AST to begin with. This is what GADT's are for, for making impossible states unrepresentable. An example of running this would be like: eval(ast)
# Returns Int(42) And trying to 'eval' an invalid 'expr: bad_ast = Eq(Value(Int(42)), Value(Bool(false))
eval(bad_ast)
# Raises an exception of "Invalid AST" Aaaand that is not the kind of error you want to see at runtime. You want to catch such errors when the AST is made, whether that is at compile-time or via some user input at runtime both, not just one or the other. Well let's encode the above Enum/ADT as a GADT instead: enum Value : t:
Bool : bool -> Value(bool)
Int : int -> Value(int
enum Expr : t:
Value : Value(t) -> Expr(t)
If : (Expr(bool), Expr(t), Expr(t)) -> Expr(t)
Eq : (Expr(t), Expr(t)) -> Expr(bool)
Lt : (Expr(int), Expr(int)) -> Expr(bool) Using an OCaml'y syntax here for the types, but in essence it types each head individual so they can only accept either a certain value type or anything and thus it types the overall Expr for that specific head in a certain way, 'constraining' it, and trying to use it in a way that doesn't match the constraint will fail to compile. Now the def eval(expr): # `Expr(t) -> t` is the type of this function
match expr: # Just matching on the enum/adt head
case Value(Bool(b)): b
case Value(Int(i)): i
case If(b, l, r):
if eval(b):
eval(l)
else:
eval(r):
case Lt(a, b): eval(a) < eval(b)
case Eq(a, b): eval(a) == eval(b) Notice how it directly maps and all to constructs as you'd expect it to be. It can be used like: eval(ast)
# Returns 42, no wrapping needed in a value or anything And it can return another type, it knows what to return because of the constrains of the ast type passed in: astb = If(Eq(Value(Int(2)), Value(Int(2))), Value(Bool(true)), Value(Bool(false)))
eval(astb)
# Returns true, all type safe And trying to mis-use it won't even compile: bad_ast = Eq(Value(Int(42)), Value(Bool(false))
# Won't even compile in typed mode as bad_ast is improperly formed, won't even reach eval
eval(bad_ast) I'd expect an error message like: bad_ast = Eq(Value(Int(42)), Value(Bool(false))
^^^^^^^
Error: This expression has type Value(int)
But an expression was expected of type Value(bool)
Type int is not compatible with type bool As is common in other languages with GADT's (this one is OCaml'y, but I use OCaml a lot so I know what to expect from it). You wouldn't even be able to even craft runtime code that parsed a user input to be able to generate an invalid AST as lacking the enforcement checks makes the type more generic than what the constraint allows for, so you'd be forced to check that the user input is valid before even generating the part of the AST structure that you are currently trying to craft. TL;DR: ADT's are a more generic and useful Enum, should do them and they work fantastically and fine in the untyped world as well. If you want to lean more on the typed world then should go with GADT's as they can add a whole useful hoard of improvements that allow for significantly smaller code and compile-time instead of runtime checks. |
Im using this a lot in java and think its a good comfort-method. |
Closing as the proposal #21014 was rejected. There are however still valid concerns/use cases raised in this issue, which should be discussed further in a new issue as mentioned on #21014 (comment) (will link it once it's open). |
Follow-up issue: #31896 |
Godot version:
Godot 3.1
Issue description:
ergo, if strings are types that support enum representation in the editor, then GDScript should logically support me creating enums of that type.
Steps to reproduce:
What do you guys think?
The text was updated successfully, but these errors were encountered: