Hey! Around 3 years ago I created a dictionary wrapper titled “dictionaryPlusPlus” (a play on C++, get it?). Unfortunately, my wrapper was pretty bad, to say the least. Most methods that were added in to help were just unnecessary overhead, recursive calls on end, it had no support for locking/constant dictionaries. It wasn’t good.
3 years later (and with a lot more programming knowledge, ehem) I’ve decided to revisit the idea of making a better dictionary system for Roblox. So I present to you NyxMap.
What Is It?
NyxMap is a hybrid between a data structure(such as a Vector3 or CFrame) and a wrapper. What do I mean by that? I mean it acts as it’s own data structure due to it keeping in check: counting, locking, capacity, and more, but acts like a wrapper in the sense that the main idea of the class is still to eventually return to you a dictionary and nothing more.
There’s a lot to get into with this module, so let’s start!
Getting Started
To create a new map with NyxMap, it’s as simple as doing:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local NyxMap = require(ReplicatedStorage.NyxMap)
local map = NyxMap.New()
There’s also a second constructor:
local map = NyxMap.With(_settings)
Which we will get into later.
If you use the accessor operator( . ) or the self operator( : ) you might notice some properties and methods are prefixed with an underscore. Why?
The prefix is to indicate this is something private and not to be touched by anyone outside of the class itself. This isn’t to hinder you from messing around, though!
For example, where are all of your own values stored inside of your map? They’re stored in a special private table called “_KVs”(keep in mind, as whenever I refer to _KVs, I am referring to your own created key/value pairs).
If you want to edit anything inside of the special _KVs table manually(map._KVs[key] = value), you’re free to do so, but editing any property or method that’s prefixed with the underscore can result in undefined and broken behavior.
It is always preferred to call the direct methods inside of NyxMap to ensure stability.
It’s important to know these details for the rest of the showcase.
The Basics
We have a few methods to modify our map safely while also keeping track of count properly without much overhead.
For now we’ll talk about the most common one: Map:Set(key, value)
Take for instance this code:
local NyxMap = require(ReplicatedStorage.NyxMap)
local map = NyxMap.New()
map:Set("Foo", "Bar")
Set, much like map._KVs[key] = value, overwrites already existing keys, while making new ones if _KVs[key] = nil, but is preferred due to a few notable features such as counting and capacity, which we will get into more later. Now, how can we access our key? We could try: map._KVs["Foo"] and that would work, but to make it easier, you can simply type:
map:Set("Foo", "Bar")
print(map:Get("Foo"))
Which will return either “Bar” or nil depending on if “Foo” exists.
We also have a simple remove method as well:
map:Set("Foo", "Bar")
map:Remove("Foo")
(Keep in mind Set, Get, and Remove only do so at the top level of _KVs. Anything past the first layer of _KVs is not checked.)
Paths
Now, how about something nested? We can’t use Set() because it only sets at the top level, so how do we get around this? Well, there’s a method for it, of course!
SetAt(location, key, value) is the method precisely. Here’s how it would look like in practice:
local NyxMap = require(ReplicatedStorage.NyxMap)
local map = NyxMap.New()
map:SetAt("Hello/World", "!", 0)
Now, before we continue, we need to understand what that first argument is taking in exactly. It’s called location(or path, used interchangeably), location is just a string, but what it represents is nesting. Similar to how on a computer “Users” is inside of the C: drive, and is represented in the hierarchy as C:\Users.
Internally, SetAt calls another method EnsurePath(location), which makes sure that the path you’re attempting to write to exists. If it doesn’t find any table or sub-table inside of the path, it creates it.
So even though our map doesn’t currently have the tables Hello or World in it, it will create them. So if we print out:
local map = NyxMap.New()
map:SetAt("Hello/World", "!", 0)
print(map._KVs)
We get:
{
["Hello"] = {
["World"] = {
["!"] = 0
}
}
}
Just like Set, SetAt has its own variant RemoveAt which takes in the same arguments as SetAt.
local map = NyxMap.New()
map:SetAt("Hello/World", "!", 0)
map:RemoveAt("Hello/World", "!")
print(map._KVs)
--[[
["Hello"] = {
["World"] = {}
}
]]
There’s also GetAt which is the same idea.
Metadata
What is metadata? If you’re unfamiliar with it, it’s essentially just data about other data. In our case, your keys and values are the actual data, whereas metadata would be how many keys there are.
When you create a new map with NyxMap.New() you get back a table, if you look inside that table, you’ll see there’s 2 table inside of itself. The first is _KVs(which we already talked about) and the second is _Metadata. As with anything underscored, it’s not meant to be manually edited.
The second constructor we talked about earlier NyxMap.With(_settings) actually allows you to create a map with certain settings safely. For example:
local map = NyxMap.With({
_Capacity = 10
})
At the top of each metadata details, you’ll either see “With Prohibited” or “[Tag], With Permitted” depending on compatibility with NyxMap.With()
What does NyxMap actually store inside _Metadata, though? The most useful two are counters. We have both: map:Count() and map:GetRootCount()
Counting
Prohibited
map:Count() will return the total count of all key/value pairs(including nested keys), where as map:GetRootCount() will return only the top level(_KVs) count.
local map = NyxMap.New()
map:Set("Test0", 0)
map:Set("Test1", 1)
map:Set("Test2", 2)
map:SetAt("Test0", "Test4", 4)
print(map:Count()) -- 4
print(#map) -- 4
print(map:GetRootCount()) -- 3
You can also call #map just like you would a regular array. #map returns map:Count() not map:GetRootCount(). That can be edited to return map:GetRootCount() instead, but that’s for later.
Capacity
_Capacity, With Permitted
The capacity metadata allows you to manually impose size limits on your map. Need a map with a maximum of 5 keys? That’s doable.
local players = NyxMap.With({
_Capacity = 10
})
Attempting to add any keys past 10 will result in a warning.
If you’d like to later update the size of your map, you’re allowed to do so, although you can only increase the size of your map, not decrease it:
local players = NyxMap.With({
_Capacity = 2
})
players:Set("cassie", true)
players:Set("nick", true)
players:Set("bob", true) -- Warning!!! "bob" not added to map
players:Resize(4)
players:Set("joel", true) -- Adds "joel" to the map
players:Set("olivia", true)
players:Set("emma", true) -- Warning!!! "emma" not added to map
players:Resize(3) -- Warning!!! New capacity can't be lower than old one
If you want a fixed capacity you can also set the _IsFixedCapacity metadata tag to true in your With constructor.
local players = NyxMap.With({
_Capacity = 100,
_IsFixedCapacity = true
})
players:Resize(200) -- Warning!!! We can't change the capacity of a fixed capacity map
There are more metadata tags you can set, these are just the basic ones.
Mutability
By default, all maps made with NyxMap are completely mutable by default. This means you can edit anything from your own keys to the private members of the map itself such as _Metadata manually.
If you’d like to make your map temporarily immutable, you can do so by either setting the _IsLocked tag to true in your With constructor, or by doing:
local guns = NyxMap.New()
guns:Set("AK-47", { ["Damage"] = 30 }) -- OK!
guns:Lock()
guns:Set("G-18", { ["Damage"] = 14 }) -- Warning! We're currently locked!
guns:Unlock()
guns:Set("G-18", { ["Damage"] = 14 }) -- OK!
While in a locked state, you are unable to: set, remove, edit capacity, etc.
You might use this during a round based game, where you only want certain players to be able to get in to the round, then lock the map for safety so no other players get join after a certain point.
But what if you want something really locked down? Something you can never edit? To ensure that this is something you really want to do, there is no metadata tag, you must call map:MakeConst(). Once you call MakeConst, the map is permanently locked down and in a read-only state.
local guns = NyxMap.New()
guns:Set("AK-47", { ["Damage"] = 30 })
guns:Set("G-18", { ["Damage"] = 14 })
guns:MakeConst()
guns:SetAt("AK-47", "Damage", 1000) -- Not OK! This map is permanently read-only
Looping
We have two loop methods: map:For() and map:ForEvery().
local guns = NyxMap.New()
guns:Set("AK-47", { ["Damage"] = 30 })
guns:Set("G-18", { ["Damage"] = 14 })
guns:For(function(key, value)
print(key) -- AK-47, G-18
print(value) -- { ["Damage"] = 30 }, { ["Damage"] = 14 }
end)
In practicality, map:For() exists so you don’t have to manually access map._KVs within your own for loop, while map:ForEvery is a little more specific. map:ForEvery acts similar to how GetDescendants() works when called on an instance. It’s more overhead due to having to loop through every key/value pair, but if you need to go through everything, it exists.
local guns = NyxMap.New()
guns:Set("AK-47", { ["Damage"] = 30 })
guns:Set("G-18", { ["Damage"] = 14 })
guns:ForEvery(function(key, value)
print(key) -- AK-47, G-18, Damage, Damage
print(value) -- { ["Damage"] = 30 }, { ["Damage"] = 14 }, 30, 14
end)
Copying
There’s two types of copying: map:Copy() and map:DeepCopy(). Let’s talk about the map:Copy() for now.
map:Copy() returns a shallow copy of itself, including methods, metadata, and key/value pairs. Because it’s a shallow copy, editing or adding new keys to this copy will also edit the original map itself as well:
local pizzas = NyxMap.New()
pizzas:SetAt("Pepperoni Pizza", "Toppings", {"Pepperoni"})
pizzas:SetAt("Hawaiian Pizza", "Toppings", {"Pineapple", "Ham"})
local otherPizzas = pizzas:Copy() -- Editing this will edit "pizzas" as well
otherPizzas:SetAt("Sausage Pizza", "Toppings", {}) -- This will add "Sausage Pizza" to our original pizzas variable as well
If you’d prefer an independent copy, you need to call map:DeepCopy()
local pizzas = NyxMap.New()
pizzas:SetAt("Pepperoni Pizza", "Toppings", {"Pepperoni"})
pizzas:SetAt("Hawaiian Pizza", "Toppings", {"Pineapple", "Ham"})
local otherPizzas = pizzas:DeepCopy() -- Editing this won't affect "pizzas"
otherPizzas:SetAt("Sausage Pizza", "Toppings", {}) -- This will only affect itself and not "pizzas"
Keep in mind that map:DeepCopy() is more expensive of a method call. The overhead can be worth it some times, but as a map grows, you’d prefer to use map:Copy() most of the time.
Merging
We can call map:Merge(otherMap). Keep in mind map:Merge() only merges non existing keys and does not overwrite keys of the same name.
local fruits = NyxMap.New()
fruits:Set("Apples", 7)
fruits:Set("Bananas", 8)
fruits:Set("Strawberries", 10)
local shoppingCart = NyxMap.New()
shoppingCart:Set("Apples", 2)
shoppingCart:Set("Cereal", 1)
shoppingCart:Set("Strawberries", 6)
shoppingCart:Merge(fruits)
print(shoppingCart) -- Apples 2, Bananas 8, Cereal 1, Strawberries 6
Equality
We can also check for equality between two maps with map:IsShallowEqual(otherMap) and map:IsEqual(otherMap)
map:IsShallowEqual() compares the values of otherMap,_KVs to self. So if otherMap._KVs contains keys that don’t exist in self, it will still return true, as long as otherMap._KVs[key] == self._KVs[key]
local fruits = NyxMap.New()
fruits:Set("Apples", 7)
fruits:Set("Bananas", 8)
fruits:Set("Strawberries", 10)
local copy = fruits:DeepCopy()
copy:Set("Pears", 2)
print(fruits:IsShallowEqual(copy)) -- true
copy:Set("Apples", 10)
print(fruits:IsShallowEqual(copy)) -- false
map:IsEqual() will look through every key value pair in otherMap._KVs and compare it to self._KVs.
local fruits = NyxMap.New()
fruits:Set("Apples", 7)
fruits:Set("Bananas", 8)
fruits:Set("Strawberries", 10)
local copy = fruits:DeepCopy()
copy:Set("Pears", 54)
print(fruits:IsEqual(copy)) -- false
print(copy:IsEqual(fruits)) -- true, because copy contains everything fruits does + similar keys are the same value
copy:Set("Apples", 10)
print(fruits:IsEqual(copy)) -- false
By default, checking for equality by doing map == otherMap will use map:IsShallowEqual, but this can be edited.
Editing Map Features
You can edit map features(such as what type of equality to use by default) by going to where you have the NyxMap module located, then opening the hierarchy of NyxMap, and opening MapSettings. The current settings are:
UseDeepEqDefault = false Set this to true if you’d prefer to use deep equality everywhere by default(map == otherMap)
LenReturnsRootCount = false Set this to true if you’d prefer the length operator(#) to return map:GetRootCount() instead of the total count
AllowAllCustomization = false Not meant to be changed except for debugging purposes. This allows you to edit prohibited settings in NyxMap.With() constructor.
Download
You can either download the source files from GitHub or you can download the module from Roblox
Contributing
For now there’s no official way to contribute, if you’d like to give any criticisms or feedback, you can reply to this post.
I may make a GitHub repo where people can contribute to one day, though.
Donating
I don’t have any formal way for you to donate, but if you’d like to, you can purchase this shirt.