Techioz Blog

JavaScript でオブジェクトを数えるために Hash.new(0) をデフォルト値 0 で宣言するにはどうすればよいですか?

概要

数字の配列を反復処理し、配列内で各数字が何回見つかったかを数えようとしています。

Ruby では簡単で、Hash.new(0) を宣言するだけで、そのハッシュは値として 0 からカウントするようにすでに設定されています。例えば:

arr = [1,0,0,0,1,0,0,1]
counter = Hash.new(0)
arr.each { |num| counter[num] += 1 } # which gives {1=> 3, 0=> 5}

同じことを JavaScript で実行したかったのですが、 counter = {} とすると、 { ‘0’: NaN, ‘1’: NaN } が返されます。

同じハッシュを JavaScript でオブジェクトとして作成する方法をご存知ですか?

解決策

ECMAScript には、Ruby がハッシュに対して行うのと同じように、オブジェクト内の欠落キーに対するデフォルト値がありません。ただし、ECMAScript プロキシ オブジェクトを使用して、動的な内省的メタプログラミングを使用して同様のことを行うこともできます。

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

したがって、次のようなことができます。

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] } ]

ただし、これは依然として Ruby バージョンと同等ではありません。Ruby バージョンでは、ハッシュのキーは任意のオブジェクトにすることができますが、ECMAScript オブジェクトのプロパティ キーは文字列とシンボルのみにすることができます。

Ruby ハッシュに直接相当するものは、ECMAScript マップです。

残念ながら、ECMAScript マップにもデフォルト値はありません。オブジェクトに使用したのと同じトリックを使用してプロキシを作成することもできますが、Map の get メソッドへのアクセスをインターセプトし、引数を抽出したり、has を呼び出したりする必要があるため、面倒になります。

幸いなことに、マップはサブクラス化できるように設計されています。

class DefaultMap extends Map {
    #defaultValue;

    constructor(iterable=undefined, defaultValue=undefined) {
        super(iterable);
        this.#defaultValue = 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

これにより、次のようなことが可能になります。

arr.reduce(
    (acc, el) => acc.set(el, acc.get(el) + 1), 
    new DefaultMap(undefined, 0)
)
//=> DefaultMap [Map] { 1 => 3, 0 => 5 }

もちろん、とにかく独自のマップの定義を開始したら、そのまま最後まで進めることもできます。

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(2) [Map] { 1 => 3, 0 => 5 }

これは、データ構造の選択がアルゴリズムの複雑さに大きく影響する可能性があるという、非常に重要な教訓も示しています。データ構造 (ヒストグラム) を正しく選択すると、アルゴリズムは完全になくなり、データ構造をインスタンス化するだけになります。

同じことがRubyでも当てはまることに注意してください。適切なデータ構造 (Web 上には MultiSet の実装がいくつか存在します) を選択すると、アルゴリズム全体が消えて、残るものは次のとおりです。

require 'multiset'

Multiset[*arr]
#=> #<Multiset:#5 0, #3 1>