Am I using Enums correctly? If so, is there a performance difference compared to strings?

I remember @oprypin helped me a while back with game commands using Enums instead of strings.

I’m at the point where there are quite a bit of game commands. Not just 20, but far, far more. I’ve gotten to the point that a command has its own sub-commands. This got me thinking why am I sending over the command as a string, when it could be an int?

Mock-up without Enums:

require "socket"
require "json"

class Client
  property socket : TCPSocket?
  
  
  def send(msg, cmd = "SERV")
    new_message = {cmd: cmd, message: msg}.to_json
    #socket.write_bytes(new_message.bytesize)
    #socket.send new_message
    pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd} to the client..."
  end
end

p = Client.new


p.send ({portal_idx: 1}), "UPDATE_ENTITY"

Mock-up using Enums (if I am doing it correctly?):

require "socket"
require "json"

enum Command
  XFER
  UPDATE_ENTITY
  TRAVEL_TO
end

class Client
  property socket : TCPSocket?
  
  
  def send(msg, cmd = "SERV")
    new_message = {cmd: cmd, message: msg}.to_json
    #socket.write_bytes(new_message.bytesize)
    #socket.send new_message
    pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd} to the client..."
  end
end

p = Client.new


p.send ({portal_idx: 1}), Command::UPDATE_ENTITY

Positives of using Enums:

  • Eases the mind of a developer (worry less about all the added string commands, and possible performance degradation). This will make the developer feel more free, and NOT stunt their thoughts about new features that could be added to the game.
  • Less bandwidth will be used since you are only sending an int value, not an entire string.
  • Faster when reading game commands on the client/server, because the if conditions only check int values, not a 13-character string.

Negatives of using Enums:

  • The client (Godot in this case), has to have the same Enums as well. So when you add a game command on the server, you need to update the Enums on the client. Now, this is not really a big issue for me, since it’s a client->server relationship. However, it is an added step a developer must do.

  • Using Command::ENTITY_UPDATE is more verbose syntactically than "ENTITY_UPDATE". However, at the same time… it’s really not because it maps to an integer value, which is far less verbose than a 13-character string. So… it’s actually a positive?

Am I thinking correctly about Enums? Or am I delusional?

I have no one to talk to about it, so I don’t really know if it makes sense. What are your thoughts?

I think there is obviously not a difference in performance on a small scale. However, if your commands are running in a 20-30hz game loop, and more players are online, this will increase the times a command is executed (more comparison executions). I’m sure there comes a point where comparing a 13-character string is slower than comparing just an int value.

I’m not sure on how to test this though, @Blacksmoke16 might be able to help with a nice benchmark script. I’ll try to create one, but I usually do them wrong :stuck_out_tongue:

Enums are essentially int values mapped to a name. They are best used to represent specific constant values, like the status of something (Active, Inactive, Pending, etc). So yes using an enum for this makes sense, it also gives you more type safety as you can add type restrictions based on your Command enum, which would prevent you from using an invalid one.

Couldn’t you make some ECR file that would generate the enum for your godot code? IDK what it actually looks like, but i imagine it wouldn’t be that hard to just generate some JSON or python or whatever.

There is also the symbol casting feature, which would allow you to do like

enum Nums
  One
  Two
end


def foo(val : Nums)
  puts val.value
end


foo :two # => 1

Vs having to type out the full name of the enum member.

1 Like

I don’t know either, haha. Probably. I only have to alt-tab to Godot, press enter and type in the same command again. (GDScript’s Enums are quite simple as well). Doesn’t really bother me, but just had to think of something.

I’m going to switch to Enums ASAP!

edit: I’m going to be using

enum CMD
  XFER
  UPDATE_ENTITY
  TRAVEL_TO
end

Syntaxically speaking, CMD::UPDATE_ENTITY is actually really nice looking IMO compared to Command:UPDATE_ENTITY. And the enum name is shorter, I like that!

Oh wow, I see what you mean now.

require "socket"
require "json"

enum Command
  XFER
  UPDATE_ENTITY
  TRAVEL_TO
end

class Client
  property socket : TCPSocket?
  
  
  def send(msg, cmd : Command)
    new_message = {cmd: cmd.value, message: msg}.to_json
    #socket.write_bytes(new_message.bytesize)
    #socket.send new_message
    pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd.value} to the client..."
  end
end

p = Client.new


p.send ({portal_idx: 1}), :UPDATE_ENTITY

I don’t need to use the enum name at all… wtf. That’s even better!

So far so good. Any thoughts / improvements? (outside of I need to use from_json).

require "socket"
require "json"

enum CMD
  XFER
  UPDATE_ENTITY
  TRAVEL_TO
end

class Client
  property socket : TCPSocket?
end

p = Client.new

def game_handler(msg, p)
  cmd = msg["cmd"].as_i
  _enum = CMD.from_value(cmd)
  case _enum
  when CMD::XFER
    pp "hi 1"
  end
rescue e
  pp "Invalid Data Received:"
  pp e
end

incoming_message = JSON.parse (%({"cmd": 0, "message": "hello"}))

game_handler(incoming_message, p)

You could replace CMD::XFER with .xfer? in your case statement, is a bit cleaner.

And yes with from_json you could have a converter that automatically converts the int value you’re sending into a Cmd enum instance.

require "json"

enum Cmd
  XFER
  UPDATE_ENTITY
  TRAVEL_TO
end

module EnumConverter(E)
  def self.from_json(pull : JSON::PullParser)
    E.from_value pull.read_int
  end
end

struct Message
  include JSON::Serializable
  
  @[JSON::Field(converter: EnumConverter(Cmd), key: "cmd")]
  getter command : Cmd
    
  getter message : String
end
  
  
Message.from_json %({"cmd":1,"message":"SOME MESSAGE"}) # => Message(@command=UPDATE_ENTITY, @message="SOME MESSAGE")
1 Like

That looks nice, but honestly, I don’t like annotations. Gosh they really do steal from the language’s beautiful syntax.

Why doesn’t .XFER? work?

I get

syntax error in eval:20Error: unexpected token: ? (expecting ‘,’, ‘;’ or ‘’)

I’d like to stay consistent with the letter casing. I like them all capitalized

Because it’s a method, and methods can’t start with an uppercase letter.

Enums automatically define methods that check if the current instance is that member. All the methods are down snakecase, as is the standard naming practice for methods in Crystal.

There should be a shorthand notation for CMD::XFER I think. That’s not fair that lower case .xfer? works, but an uppercase variant doesn’t, especially considering the enum values must start with a capital letter. IMO, this promotes consistency and brevity. @asterite what are your thoughts?

I wonder if there are any github issues related to this, I want to read them ;D

I just want a way of doing it properly. I feel like there is too many choices. I’ll probably just do CMD::XFER, so I can keep the consistent casing

Now that I think of it, that’s really weird because first, Enum values must start with a Capital letter. Second, having lowercase methods to check enums… like .xfer? in this case, is the opposite of brevity, it’s more confusing, because it doesn’t even match the Enum value’s casing. A method is not an Enum’s value LOL

I’ve created a GitHub issue about this issue: https://github.com/crystal-lang/crystal/issues/8244

@girng_github You have two options here:

  1. You can use the full enum name, CMD::XFER in your case statement
  2. You can use the auto generated methods .xfer?

Having something essentially equivalent to #2 with the only difference being the method name is all uppercase doesn’t provide much benefit.

Granted if you really wanted to use like :XFER you could probably add some overload to do that comparison, as mentioned in https://github.com/crystal-lang/crystal/issues/6592#issuecomment-415393518

Yeah, I really don’t mind using CMD::XFER, however, I feel CMD::XFER is far more explicit, more consistent with how Enums function (values must start with a capital letter), and offers more brevity. I don’t understand why the implicit-object syntax was added for Enums when it’s the total opposite of all the positives of using CMD::XFER.

Probably because Enums can have methods.

However, if used in a case when statement, it’s weird because those methods don’t match the values of the Enum values

Mainly since

color == Color::Red
# vs.
color.red?

The latter is easier to read/write.

I mean its you’re opinion its weird but in practice it’s not a big deal.

Are free to use either or so do whatever makes more sense to you. I prefer the .xfer? style since its less typing.

Problem I have is, why is the language being prejudiced against developers who prefer to check their Enum values with the actual value of the Enum, but on the other hand, offers short-hand notation through implicit-object syntax that does not even correspond to the actual Enum value itself (XFER → xfer)?

Sorry, but that just doesn’t make sense to me. It should work both ways!

I’d argue the implicit-object syntax for Enums is hard to read code and honestly, shouldn’t even work for Enums because it’s confusing af.

Consistency and less ambiguity is very important.

This also is not a personal opinion, I’ve stated objective facts in my GitHub issue btw… I tried, oh well

Using implicit-object like syntax for enum value checking, is equivalent to



test = Hash(String, String).new

tEST["e"] = "hi"

It makes no sense. The casings do not even match. And Enum values must start with a capital letter… come on now, I’m not that stupid.

If you’re so adamant on having the casing match you are free to define your own way of doing things for your projects, no matter what anyone else thinks. Something like:

struct Enum
  def is?(member : self)
    self == member
  end
end

enum Color
  Red, Green, Blue
end

c = Color::Green
case c
when .is?(:Red)
  puts "It's red"
when .is?(:Green)
  puts "It's green"
when .is?(:Blue)
  puts "It's blue"
end

Or crazier still:

struct Enum
  def []?(member : self)
    self == member
  end
end

enum Color
  Red, Green, Blue
end

c = Color::Green
p c[:Green]?

Get creative!

Enum#is? is the way I want it to be in the end: https://github.com/crystal-lang/crystal/issues/8000

I’d like to remove those question methods.

@Exilor Now that I see it, it’s funny that everyone :+1: but you :-1:

Thank you @Exilor for the examples. Honestly, I like being more explicit and using CMD::XFER instead of using implicit-object syntax, because the case when is simply matching the value of an Enum. I don’t feel like it needs to do anything more than that.

Just to add: it’s not about me. This is objectively about good programming practices. It’s consistent, offers more brevity, and matches the actual Enum value itself (not lowercase!). IMO, these 3 things offer a clear advantage over an implicit-object syntax for Enum value checking. This is objectively true, not a personal statement.

1 Like