How to dynamically create a NamedTuple for double splatting?

def generate_item_by_id(id, quantity = 1, cold_res = 0, fire_res = 0, lightning_res = 0)
  {id, quantity, cold_res, fire_res, lightning_res}
end


mods_to_roll = {"cold_res", "fire_res", "lightning_res"}
mods = Hash(String, Int32).new

3.times do
  mods[mods_to_roll[rand(mods_to_roll.size)]] = 20
end

pp mods

pp generate_item_by_id(5, **NamedTuple.from(mods))

https://play.crystal-lang.org/#/r/7ztn

In /usr/lib/crystal/named_tuple.cr:53:24

 53 | NamedTuple.new(**{{T}}).from(hash)
                         ^
Error: undefined constant T

I believe this doesn’t work because NamedTuple needs the keys and types. However, they are chosen at random.

I don’t know if I am going about this the right way, though. Any tips / advice is appreciated

NamedTupeles can’t be created dynamically (outside of macro land) so you definitely need to rethink things. If this is a one off the solution is super easy. You know the number of properties you’re assigning, so just randomly generate that number of items and assign them manually.

https://play.crystal-lang.org/#/r/7zuy

If you need a more robust solution, macros are probably the answer.

Thanks @watzon
I might have to rethink this

I have an idea where I can pass in a Hash, iterate over it, then update the local variables to their respective value? So if fire_res's isn’t in the Hash, its local variable (fire_res) gets set to its default value. If it is in the Hash, it’s set to the value it has in the Hash.

Seems hacky, but I think it might work?

Won’t work without a macro. Technically you could probably use method_missing to do that, but it wouldn’t be recommended. If each item is an individual class that inherits from a parent Item class it would be pretty easy to write a generator method in the base class that iterates over class variables.

@watzon Check this out!! I got something working!!

def generate_item_by_id(id, mods)
  quantity = 1
  cold_res = 0
  fire_res = 0
  lightning_res = 0
  
  cold_res = mods["cold_res"] if mods["cold_res"]?
  fire_res = mods["fire_res"] if mods["fire_res"]?
  lightning_res = mods["lightning_res"] if mods["lightning_res"]?
        
  {id, quantity, cold_res, fire_res, lightning_res}
end
      
mods_to_roll = {"cold_res", "fire_res", "lightning_res"}   
mods = Hash(String, Int32).new
      
3.times do
  mods[mods_to_roll[rand(mods_to_roll.size)]] = 20
end
      
pp generate_item_by_id(5, mods)

https://play.crystal-lang.org/#/r/7zvh/edit

What do you think?

I think what you’re looking for in your most recent example is Hash#fetch:

def generate_item_by_id(id, mods)
  quantity = 1
  cold_res = mods.fetch "cold_res", default: 0
  fire_res = mods.fetch "fire_res", default: 0
  lightning_res = mods.fetch "lightning_res", default: 0
        
  {id, quantity, cold_res, fire_res, lightning_res}
end

mods_to_roll = {"cold_res", "fire_res", "lightning_res"}   
mods = Hash(String, Int32).new

3.times do
  mods[mods_to_roll[rand(mods_to_roll.size)]] = 20
end

pp generate_item_by_id(5, mods)

https://play.crystal-lang.org/#/r/7zxx

I suspect that this could be more easily solved with a struct, though. How do you get your hash in the first place?

1 Like

I think you shouldn’t be using Hash nor NamedTuple, you want a type to represent an item or the item description.

2 Likes

Following up on what @asterite said, here are a couple examples.

Minor Modifications
struct Item
  getter id : Int32
  property quantity = 1
  getter cold_res : Int32
  getter fire_res : Int32
  getter lightning_res : Int32
  
  def initialize(@id, @cold_res = 0, @fire_res = 0, @lightning_res = 0)
  end
  
  def initialize(@id, mods)
    @cold_res = mods.fetch "cold_res", default: 0
    @fire_res = mods.fetch "fire_res", default: 0
    @lightning_res = mods.fetch "lightning_res", default: 0
  end
end

mods_to_roll = {"cold_res", "fire_res", "lightning_res"}   
mods = Hash(String, Int32).new
      
3.times do
  mods[mods_to_roll[rand(mods_to_roll.size)]] = 20
end

pp Item.new(5, mods)

Play Link

Significant Modifications
enum DamageType
  Cold
  Fire
  Lightning
  # Try uncommenting the next line to see how you can easily add a new damage type
  # Acid
end

class Item
  getter id : Int32
  property quantity = 1
  private getter resistances = Hash(DamageType, Int32).new

  def initialize(@id, @resistances)
  end

  def resistance(to damage_type : DamageType)
    resistances.fetch(damage_type, default: 0)
  end
end

mods_to_add = DamageType.values # for illustration, we'll just do all damage types
mods = Hash(DamageType, Int32).new

mods_to_add.each do |mod|
  mods[mod] = 20 if rand(100) > 50 # set resistance 50% of the time
  # Note: do *not* use the exact random code above. I'm certain there are better ways to do that.
end

item = Item.new(5, mods)

pp item
mods_to_add.each do |mod|
  puts "Resistance to #{mod}: #{item.resistance to: mod}"
end

Play Link

I expect you’re using Named Tuples because 1) you’re familiar with them, and 2) you expect them to be faster. A couple replies:

  1. You may be familiar with them, but it will be much easier to re-familiarize yourself with your code later if you use types that represent your data with class, method, and variable names that describe what the named class, method, or variable is for. Note the unnecessary use of to: in the “Significant Modifications” code. It will take a moment longer to type, but it will be quicker to read later when you’re debugging why (for example) lightning damage resistance is far too prevalent, making a particular enemy much easier to deal with than it should be.
  2. Named Tuples may indeed be faster in this case. However, they may also not be that much faster. You should worry about making your code well-structured, correct, and easy to read before worrying about how fast it is. It’s much easier to go back and speed up your code than to go back and fix the bug hiding in your tricky optimization.

Anyway, if you just want to use Hashes and Named Tuples, you obviously can. We’re just trying to help Present You write code that is easier to handle for Future You (or any other developers you may eventually decide to hire/collaborate with).

3 Likes

Thanks for the info everyone. I like the .fetch and default way!! I think I’m going to use that for now

I am using a Tuple because it works well for bracket notation access, its .to_json converts it into a string delimited list, and intertwines beautifully with db.query methods. Examples:

ItemQuery = "itemid, rpg_items_id, slot_position, q, equipped, sockets, identified, i_atk_speed, i_phys, i_crit_chance, crit_multiplier, i_hp, i_ms, i_cast_speed, in_item, socket_data, i_defense, to_life, quality, fire_res, cold_res, lightning_res, rare_name"
alias ItemTuple = {Int64, Int32, String, Int16, Int8, String, Int8, Int16, Int16, Int16, Int16, Int16, Int16, Int16, String, String, Int16, Int16, Int8, Int8, Int8, Int8, String}
  def db_get_user_items(client, tabid = 0)
    db.query_all "select #{ItemQuery} from rpg_user_items where rpg_character_id = ? and user_id = ? and in_stash = #{tabid} and hardcore = ? and ladder = ?", client.selected_characterid, client.user_id, client.hardcore, client.ladder, as: ItemTuple.types
  end

I tried to use a class, but it became too convoluted. Had to maintain a DB.mapping, property macros, create custom overloads for bracket notation access (which I couldn’t figure out how to do), create a to_json overload to convert all ivars into a delimited string, etc. Just became too much. A Tuple seems to do more stuff out of the box for what I need, and seems more powerful.

I use classes a lot, I just don’t think using a class for this is a good idea with my current knowledge of Crystal.

I suspect that this could be more easily solved with a struct, though. How do you get your hash in the first place?

Depends on the type of item. I have static Hashes loaded that represent the modifications to select from based on what item is being used. In the example below, it’s ModGlobalWeapons or ModGlobalArmors. The data is stored in MYSQL and exported as csv, then converted into Hashes in Crystal.

Example:

      when "transmute_orb"
        raise "Item must be normal rarity." if current_item_mods.size > 0
        mod_rolls = Array(String).new
        query_update = QueryUpdate.new

        if main_on_item["type"] == "Weapons"
          mod_rolls = ModGlobalWeapons.dup
          mod_rolls = mod_rolls.shuffle.first(2)
        else # assume boots/armors
          mod_rolls = ModGlobalArmors.dup
          if main_on_item["type"] == "Gloves"
            mod_rolls << "i_atk_speed"
          end
          mod_rolls = mod_rolls.shuffle.first(2)
        end
        mod_rolls.each do |mod_key|
          mod_struct_main = GlobalItemModsMapping[mod_key]
          mod_struct = mod_struct_main[mod_struct_main.keys.shuffle.first]
          final_roll_value = rand(mod_struct.min_value..mod_struct.max_value)
          query_update[mod_struct.mod_type] = final_roll_value
          tuple_index = GlobalItemTuple.index(mod_struct.mod_type)
          client.stash[tabid_on].items[on_itemid] = modify_item_tuple(client.stash[tabid_on].items[on_itemid], tuple_index, final_roll_value)
        end
        if query_update.size > 0
          db.exec "update rpg_user_items set #{hash_to_delimited_query_list(query_update)} where itemid = ?  and user_id = ?", on_itemid, client.user_id
          client.send ({a: 4, new_data: query_update, q: new_item_quantity, on_itemid: on_itemid, itemid: incoming_itemid}), "ITEMUPDATE"
          # Remove 1 quantity from the orb..
          client.stash[tabid].items[incoming_itemid] = modify_item_tuple(item, 3, new_item_quantity)
        else
          raise "Item use failed.."
        end