Skip to content

Ergonomic way of only adding a new object to a map if it doesn’t exist?

I am developing a game where it is very often that I wish to perform the following operation:

if (!buffs.has("shielding")) {
  buffs.add(new ShieldingBuff(50)); // 50 is the shield amount
}

where buffs type is Map<string, Buff>.

I thought to wrap it in a helper function like so:

function addBuff(buffType, buff) {
  if (!this.buffs.has(buffType) {
    this.buffs.add(buff);
  }
}

and then calling it like this:

addBuff("shielding", new ShieldingBuff(50));

But the problem with that is that in 99% of cases the buff will already be applied, and this incurs a significant garbage collection cost, since we’re allocating a ShieldingBuff object regardless of whether or not we actually need it.

I was thinking about maybe using short-circuiting somehow, but I cannot think of an ergonomic way of doing it. The best I could do is come up with something like this:

buffs.add(!buffs.has("shielding") && new ShieldingBuff(50));

which works to avoid the allocation in the case the buff is already applied.. but is still rather verbose. Ideally I would be able to encapsulate the has check into the helper function somehow, but I can’t see a way to do that while maintaining the short-circuiting trick to avoid the allocation in the majority case where the buff is already applied and nothing need be done.

My worry is that if I have to remember to call the has check every time, eventually I’ll forget and it will lead to a bug. I would like to avoid that if possible.

Answer

Something that has a bit of overhead but probably not as much as ShieldingBuff would be a simple function that, when called, creates one:

addBuff("shielding", () => new ShieldingBuff(50));
function addBuff(buffType, makeBuff) {
  if (!this.buffs.has(buffType) {
    this.buffs.add(makeBuff());
  }
}

Another approach would be to refactor your code so you don’t have to repeat 'shielding' and shieldingBuff – instead, use an object or Map for the buffs instead of having multiple standalone variables.

For example, with an object like

const buffs = {
  Shielding: function() {
    // code of ShieldingBuff here
  }
  // ...
}

Then you can do

addBuff("shielding", 50)

where, if the condition is fulfilled, addBuff capitalizes the first letter, and passes the 50 to the constructor when needed, using bracket notation to find the constructor on the buffs object.