pawn-lang / YSI-Includes

Just the YSI include files, none of the extra stuff.
211 stars 106 forks source link

Hashmap documentation #236

Open Southclaws opened 7 years ago

Southclaws commented 7 years ago

I was looking for a hashmap implementation and realised YSI probably has one, I can't really figure out the API though.

Basically, I just want to update my logging library so logging handlers can implicitly be turned on/off without needing to be specifically registered on init (how i used to do it).

Even better, I can use the __file directive from zeex' compiler as a hashmap key so I can enable/disable logging on a per-file basis.

So, basically I just want something akin to Go/Python/JS/etc:

logLevel["weapons"] = 3

I thought I understood how to allocate a hashmap structure: new HashMap:name<size>; but then read the macro and noticed the second parameter isn't used: #define HashMap:%0<%1> %0[HASH_MAP_SIZE + 4] 🤔

And I can't figure out what this part means for HashMap_Init

Finds the location of the hash map linked list data in the passed array data and uses that to read the data through pointers subsequently. It doesn't matter WHERE in the enum the hash map data is, and if its not there you'll get an error, or at least a warning.

I'd love some examples of how to use hashmaps!

Y-Less commented 7 years ago

The hashmap in YSI was designed for my purposes, which was specifically using strings to get an index in to an array, and not for any arbitrary data storage against strings. Thus, the extra parameter you are confused by is the array the map is tied to, and is actually the place where the strings are stored.

Frequently now, the best place for examples in more obscure YSI libraries are their tests:

https://github.com/Misiur/YSI-Includes/blob/bada721cea24fc00f143c57f275699c32bb3839d/YSI_Data/y_hashmap/tests.inc#L73-L88

This shows more what I'm talking about with targetting specifically indexes:

https://github.com/Misiur/YSI-Includes/blob/bada721cea24fc00f143c57f275699c32bb3839d/YSI_Data/y_hashmap/tests.inc#L98-L109

data includes storage for strings, which are used as the name of the slot that string is found in. HashMap_Add takes a map, a string, and a slot, and actually writes that string in to that slot of the bound array. You don't pass the array to HashMap_Add (which is just set basically), you passed both the array and the map to HashMap_Init and the references were stored internally.

I recently added (but haven't pushed) HashMap_AddUnused, which doesn't take a slot and instead finds an unused one. That might be useful for your purposes if you are already storing logger data in an enum array. Of course, if you just want to store a number, you can do it, but it isn't ideal:

enum E_SINGLE_INT_STORE
{
    E_SINGLE_INT_NAME[32],
    E_SINGLE_INT_DATA[HASH_MAP_DATA],
    E_SINGLE_INT_VALUE
}

static
    HashMap:gLoggersHash<>,
    gLoggersStore[MAX_LOGGERS][E_SINGLE_INT_STORE];

hook OnScriptInit()
{
    HashMap_Init(gLoggersHash, gLoggersStore, E_SINGLE_INT_DATA);
}

stock bool:Logger_Set(string:name[], value)
{
    // See if the string already exists.
    new
        slot = HashMap_Get(gLoggersHash, name);
    // Nope, get a free slot.
    if (slot == -1)
        slot = HashMap_GetUnused(gLoggersHash);
    if (slot == -1)
        return false;
    // Store the data in the slot.
    HashMap_Add(gLoggersHash, name, slot);
    gLoggersStore[slot][E_SINGLE_INT_VALUE] = value;
    return true;
}

stock Logger_Get(string:name[])
{
    // See if the string exists.
    new
        slot = HashMap_Get(gLoggersHash, name);
    if (slot == -1)
        return cellmin;
    // Get the data in the slot.
    return gLoggersStore[slot][E_SINGLE_INT_VALUE];
}

stock bool:Logger_Remove(string:name[])
{
    // See if the string exists.
    new
        slot = HashMap_Get(gLoggersHash, name);
    if (slot == -1)
        return false;
    // Delete the key from the map.
    HashMap_RemoveKey(gLoggersHash, name);
    // Add it back to the unused list.
    HashMap_AddUnused(gLoggersHash, slot);
    // "HashMap_RemoveKey" should add it automatically, but it doesn't.  The two
    // systems were developed years apart and I forgot to update both!
    return true;
}
Y-Less commented 7 years ago

Think of it akin to y_iterate - that loops over a constrained set of values, often array indices. This does the same. Mostly because 99% of the time you want more data, and that must be stored in a fixed array, so having fast access to those indices is desirable.

Anyway, if that's not what you need, sorry... You could adapt the code, take parts, or write your own entirely. Frankly just an array with strings, a second array for hashes, and some collision checks would have been simpler. I don't remember why I opted to have the strings stored in the target array instead of a new array. I think it might have been something to do with still wanting to access them directly. Or it was because I didn't want to fix the size of strings as the same in all hashmaps - but that is easy to work around!

Y-Less commented 7 years ago

Whatever the reason for the current design, I'm sure it was very cleverly solving problems no-one will ever have, became very over-engineered as a result, and ultimately totally unnecessary. I spend a lot of time now going over old YSI code and just deleting large chunks that are not needed by anyone ever. Did you know y_commands used to have per-player shortcuts, so anyone could bind a long command to a single letter command custom only to them? No, neither did anyone else...