I'm using the crystal language, and it's been great so far. Unfortunately, I feel like my code is becoming a bit too messy with types everywhere.
For example:
# ---------=====----++---
# Grab characters
# ---------=====----++---
def handle_character_list(msg, client)
result = {} of String => Array(Tuple(Int64, String, String, Int16, Int8)) | Int32 | Int16 | Int64 | String
result["characters"] = db.query_all "select character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc from rpg_characters where user_id = ? ", client.user_id,
as: {Int64, String, String, Int16, Int8}
result["max_char_slots"] = client.user_data["max_char_slots"]
puts result
end
While looking at the db.query_all method, it says:
returns an array where the value of each row is read as the given type
With the aforementioned above, why do I need to explicitly set those types again to my result
variable, if they are going to be returned?
I feel like I'm doing something wrong, and any advice/insight is appreciated.
The first thing that jumps out at me is the size of the type of your hash. You seem to be using Hash
the same way as in Ruby. Don't.
In Ruby, or other dynamic languages, Hash
es or objects are used as generic data containers, almost like unnamed classes. In Crystal, hashes have a single type for the key, and a single type for the value, which makes them unsuited for the task. You want to tell Crystal more about the structure of your data, so you don't have to keep repeating that.
The first thing to do is look at the result
object. It can simply be transformed into a record Result
:
record Result,
characters: Array({Int64, String, String, Int16, Int8}),
max_char_slots: Int32
the method then becomes
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
characters = db.query_all sql, client.user_id, as: {Int64, String, String, Int16, Int8}
max_char_slots = client.user_data["max_char_slots"]
Result.new(characters, max_char_slots)
end
However, by looking at the method it might be that this Result
record is only used in one place - to return data from this method. In that case it's unlikely you want to give it a more formal name. In this case you could use a NamedTuple
. They're a bit like an anonymous record.
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
{
characters: db.query_all(sql, client.user_id, as: {Int64, String, String, Int16, Int8}),
max_char_slots: client.user_data["max_char_slots"]
}
end
Going further, we can see that a "Character" is also a type:
class Character
getter id : Int64
getter name : String
getter created : Time
getter level : Int16
getter cc : Int8
def initialize(@id, @name, @created, @level, @cc)
end
end
We can then use the DB.mapping
to define how the Character
class looks in the database.
class Character
DB.mapping({
id: Int64,
name: String.
created: Time,
level: Int16,
cc: Int8
})
def initialize(@id, @name, @created, @level, @cc)
end
end
This definition is equivalent to the previous one because DB.mapping
automatically generates getters for us.
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
{
characters: db.query_all(sql, client.user_id, as: Character),
max_char_slots: client.user_data["max_char_slots"]
}
end
Going even further, I'd extract that into two methods, each one doing just one thing, and I'd probably make client.user_data
more type safe:
def characters_for_user(user_id)
sql = <<-SQL
SELECT character_id, character_name, created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
db.query_all(sql, user_id, as: Character)
end
def handle_character_list(msg, client)
{
characters: characters_for_user(client.user_id),
max_character_slots: client.user_data.max_char_slots
}
end
This is just my thought process on how I'd write the code you've shown. I've made a lot of assumptions about your code and database which might be wrong (i.e. "created" is a DATETIME in mysql). I'm attempting to show a thought process, not a finished solution. Hope that helps.