To illustrate the problem, consider the following simple object
function Key( val ) {
this._val = val;
}
Now I create a ES6 Map
instance and feed one entry into it like this
var map = new Map(),
key1 = new Key( 'key' );
map.set( key1, 'some value' );
console.log( 'key1: ', map.has( key1 ) );
// key1: true
So far everything is fine. The challenge, however, comes up, if I create a nearly identical object key2
like this
var key2 = new Key( 'key' );
So basically both keys are identical, but obviously key2
is not part of the map
console.log( 'key2: ', map.has( key2 ) );
// key2: false
JavaScript uses the object references as a key here, so the two separate objects will not point towards the same value.
What I would like to do now is, to add something like a hash()
method to key's prototype
, so that both object would point to the same key. Is something like this possible?
I know, that there would be a way to circumvent the problem using a factory pattern for the Key
generation together with some caching. However, this results in a lot of problem regarding immutability of the objects and the cache preventing old objects from being garbage collected. So I think that is not really an option.
HashMap is implemented by ES6 in JavaScript. It has a key-value pair and in both, we can use non-primitive datatype to store the data. The Map will return the key, value pairs in the same order we inserted. Map also have properties and methods like the size of the map and more.
Is something like this possible?
No, this is a known flaw of ES6 Collections. All they do is check for reference identity, and there is no way to change that.
The best thing you can do (if hash consing the instances is not an option as you say) is not to use objects for the keys. Instead, use strings that encode the Key
values, and convert back and forth between the two representations. Given that you consider your keys to be immutable, this should not pose a problem.
I've created a class called CanonMap in my library big-m to encapsulate mapping by hash instead of reference.
By default, it works with tuples, Dates, and simple objects:
const { CanonMap } = "big-m";
const myMap = new CanonMap();
myMap.set(
["Farooq", "867-5309"],
36.59
);
myMap.get(
["Farooq", "867-5309"]
) === 36.59;
myMap.set(
{name: "Farooq", number: "867-5309"},
36.59
);
myMap.get(
{number: "867-5309", name: "Farooq"} // Insensitive to key ordering
) === 36.59;
myMap.set(new Date(2012, 6, 5), "Tuesday");
myMap.get(new Date(2012, 6, 5)) === "Tuesday";
It can also be extended with a custom "canonizer" function that determines how to hash values:
import {naiveCanonize, jsonCanonize, JsonCanonMap, CanonMap} from "big-m";
// Same as default canonizer, but with greater recursion depth (default is 2)
new CanonMap([], 6);
// Canonize by ID with fallback to naive
const customCanonMap = new CanonMap([
[{id: "TEST1", x: 7}, 77],
[{ x: 7 }, 88]
], lookup => lookup.id || naiveCanonize(lookup));
customCanonMap.get({id: "TEST1", x: 8}) === 77; // Ignores other values, uses ID
customCanonMap.get({x: 8}) === undefined; // Uses all fields, so lookup fails
// Default canonizer with JSON.stringify
new CanonMap([], jsonCanonize);
// equivalent to
new CanonMap([], lookup => JSON.stringify(lookup));
// also equivalent to
new JsonCanonMap();
Finally, to implement a CanonMap that makes use of a prototype hash function on the object itself, as you described, you could do something like this:
const selfHashingCanonMap = new CanonMap([], lookup => {
if ("hash" in lookup) {
return lookup.hash();
} else {
return naiveCanonize(lookup);
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With