{Map, WeakMap, etc}.prototype.getRef(key)

When working with Javascript objects, we have access to a large variety of operators, such as obj.x++, obj.x ??= 2, or obj.x **= 2 to get the job done. It's unfortunately not possible to use operators such as those when accessing values from an instances of Map, WeakMap, etc. This leads to highly unreadable code to do basic operations:

// Increment a value
map.set(map.get('myKey') + 1)

// Set a non-existent value
if (!map.has('myKey')) {
  map.set('defaultValue')
}

This has caused us to start looking into creating awkward convenience functions to do what's already possible to do with these operators, like the proposed emplace() function here which attempts to solve the past two scenarios as follows:

// Increment a value
counts.emplace('myKey', {
  update: existing => existing + 1
});

// Set a non-existent value
map.emplace('myKey', {
  insert: () => 'defaultValue'
});

I propose we add a .getRef() method to Map, Set, and any other data structure that could benefit from it. It will return a reference with a "value" getter/setter property that can be used to update that specific entry. It can be defined somewhat like this:

// Note that browsers can add a bit of caching,
// so a lookup doesn't have to happen with each ref operation.
Map.prototype.getRef = function(key) {
  const self = this
  return {
    get value() {
      return self.get(key)
    },
    set value(newVal) {
      self.set(key, newVal)
    },
    exists() {
      return self.has(key)
    },
  }
}

Now we can write our previous example in a very simple, straight-forward way:

// Increment a value
map.getRef('myKey').value++

// Set a non-existent value
map.getRef('myKey').value ??= 'defaultValue'

Some other motivating examples:

// Insert 0 if doesn't exist, then increment
const ref = map.getRef(key)
(ref.value ??= 0)++

// Another insert or update example
const ref = map.getRef(computeKey())
ref.value = !ref.exists() ? createNew : updateValue(ref.value)

I'd much rather have a kind of Map that works like Python's defaultdict.

const defmap = new DefaultMap(() => ({value:0}));
defmap.get(key).value++;
// equiv to:
const map = new Map();
(map.has(key) || map.set(key, {value:0}), map.get(key)).value++;
1 Like

It does not get the reference of the key, but create a new reference every time you call it.
i.e.

map.getRef('myKey') != map.getRef('myKey') 

Good?

@simonkcleung

Yes, it would return a new object every time you request the reference, so map.getRef('myKey') != map.getRef('myKey') would be true.

@lightmare - I do think default-dicts would solve many of the problems that .getRef() solves, and maybe if that kind of feature goes in, the need for .getRef() would be lessened. But, your particular solution of having a default dict automatically create objects that you then mutate works, but it feels a little "tricky" - plus it forces you to structure the contents of the map in a specific format - you can't just use primitives as values. Still, it's an interesting idea, and it shows that default dicts can be used to achieve the same objectives.

I think I’ve got tired of the .value – I’ll be happy with a new symbol or something such that collection.getRef("property")++ is executable. I don’t know if it’s even possible though.

I guess I would like to do array.at(-2)++ too. How do you think @theScottyJam?

I don't know either. I believe in C++ it's possible for userland to write logic, that can detect when it's being assigned to, but most languages I know don't enable this sort of pattern, and maybe there's good reason for it, however, I can't think of any reasons off of the top of my head as to why this would be a bad pattern to enable.

I don't think it is possible because it is an invalid syntax.

C++ has Type::operator =(Type&), so yes, it's very possible there. (Their standard library uses it to implement map updates, actually.) Rust has std::ops::DerefMut<T> that allows for a subset of this, but that can't be used to create a userland reference with custom logic like that.

It's not so much an issue of whether it's a good or bad pattern (keep in mind, we have proxies that let you completely virtualize property access already) but of implementation efficiency. Without such virtualized references, you can assume that what looks like a variable assignment is a mutating assignment of a single value, but with such virtualized references, you can only know that by checking the variable's type itself.

I could see such userland references getting through provided they're explicitly denoted somehow so implementations can statically tell them apart without having to do type analysis first.

1 Like

Did you link to the wrong thing? I don't see the connection to this idea from that thread.

Either way, if we make special syntax to make this sort of thing statically analyzable, we'll probably be back to something similar to the idea I originally had, that pre-dated this one, which was to make special syntax for accessing and updating entries of a data structure, so one could do so native language syntax instead of function calls. Which, perhaps that sort of thing would satisfy @graphemecluster's desire as well.

It's a kind of dynamic reference that's statically analyzable, just one intended to be built on decorators.

1 Like

What I understand is that the expression for a postfix operation cannot be a function call.
ie. array.at(-2)++ will result in a syntax error before running array.at(-2).

It's a ReferenceError rather than a SyntaxError. So run-time not parse-time.

let m = new Map([['a', 1]]);
// no syntax error to declare this function
function f() {
 return m.get('a')++;
}
// error happens when code runs
f() // Uncaught ReferenceError!

However the following is a SyntaxError:

function f() {
 return (0, m.get('a'))++;
}
// Uncaught SyntaxError!