ECMAScript does not have default values for missing keys in objects the same way Ruby does for Hash
es. You can, however, use dynamic introspective metaprogramming to do something similar, using ECMAScript Proxy
objects:
const defaultValue = 42;
const proxyHandler = {
get: (target, name) => name in target ? target[name] : defaultValue
};
const underlyingObject = {};
const hash = new Proxy(underlyingObject, proxyHandler);
1 in hash
//=> false
1 in underlyingObject
//=> false
hash[1]
//=> 42
underlyingObject[1]
//=> undefined
So, you could do something like this:
arr.reduce(
(acc, el) => { acc[el]++; return acc },
new Proxy(
{},
{ get: (target, name) => name in target ? target[name] : 0 }
)
)
//=> Proxy [ { '0': 5, '1': 3 }, { get: [Function: get] } ]
However, this is still not equivalent to the Ruby version, where the keys of the Hash
can be arbitrary objects whereas the property keys in an ECMAScript object can only be String
s and Symbol
s.
The direct equivalent of a Ruby Hash
is an ECMAScript Map
.
Unfortunately, ECMAScript Map
s don't have default values either. We could use the same trick we used for objects and create a Proxy
, but that would be awkward since we would have to intercept accesses to the get
method of the Map
, then extract the arguments, call has
, and so on.
Luckily, Map
s are designed to be subclassable:
class DefaultMap extends Map {
constructor(iterable=undefined, defaultValue=undefined) {
super(iterable);
Object.defineProperty(this, "defaultValue", { value: defaultValue });
}
get(key) {
return this.has(key) ? super.get(key) : this.defaultValue;
}
}
const hash = new DefaultMap(undefined, 42);
hash.has(1)
//=> false
hash.get(1)
//=> 42
This allows us to do something like this:
arr.reduce(
(acc, el) => acc.set(el, acc.get(el) + 1),
new DefaultMap(undefined, 0)
)
//=> DefaultMap [Map] { 1 => 3, 0 => 5 }
Of course, once we start defining our own Map
anyway, we might just go the whole way:
class Histogram extends DefaultMap {
constructor(iterator=undefined) {
super(undefined, 0);
if (iterator) {
for (const el of iterator) {
this.set(el);
}
}
}
set(key) {
super.set(key, this.get(key) + 1)
}
}
new Histogram(arr)
//=> Histogram [Map] { 1 => 3, 0 => 5 }
This also demonstrates a very important lesson: the choice of data structure can vastly influence the complexity of the algorithm. With the correct choice of data structure (a Histogram
), the algorithm completely vanishes, all we do is instantiate the data structure.
Note that the same is true in Ruby also. By choosing the right data structure (there are several implementations of a MultiSet
floating around the web), your entire algorithm vanishes and all that is left is:
require 'multiset'
Multiset[*arr]
#=> #<Multiset:#5 0, #3 1>