疑似乱数のユーティリティ函数を提供する Stage 1 Random Functions
変更情報
【2025/06/29】
- Random Collection Functions の
Random.pop
についてMap
を受け取った場合キー、バリュー両方返すよう記述を修正 - Seeded Pseudo-Random Numbers についての記述を追加
現状の JavaScript における疑似乱数 API
JavaScript における疑似乱数 API は、ECMAScript としては 0 以上 1 未満の値を一様乱数で返す Math.random
が、Web Crypto API としては暗号論的疑似乱数を返す crypto.getRandomValues
が定義されています。必要最低限な API のみが提供されているのが現状です。
例えば六面ダイスの値を返す函数を作ろうと考えた場合に、Math.random
を使って以下のように定義する必要があります。
function getRandomDiceRoll() {
return Math.floor(Math.random() * 6) + 1;
}
他にも配列をシャッフルするには Fisher-Yates アルゴリズム実装を用意する必要があります。
function shuffle(array: ArrayLike<unknown>) {
for (let currentIndex = array.length - 1; currentIndex > 0; --currentIndex) {
const randomIndex = Math.floor(Math.random() * (currentIndex + 1));
// currentIndex と randomIndex の値をスワップする
const temp = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temp;
}
}
これらをわざわざ用意するのは手間な上、擬似乱数を使ったコードはその性質上実装が誤っていても気づきにくい問題があります。そこで ECMAScript に疑似乱数のユーティリティ函数(特に配列のシャッフル)を入れることが望まれてきましたが、長いこと達成されませんでした。
Stage 1 Random Functions
2025年5月の TC39 会議にて、新たにグローバルに Random
ネームスペースを定義し、いくつかのユーティリティ函数を提供する提案が出され Stage 1 となりました。
namespace Random {
function number(lo: number, hi: number, step?: number): number;
function int(lo: number, hi: number, step?: number): number;
function bigint(lo: bigint, hi: bigint, step?: bigint): bigint;
type BufferType =
| Int8Array<ArrayBuffer>
| Uint8Array<ArrayBuffer>
| Uint8ClampedArray<ArrayBuffer>
| Int16Array<ArrayBuffer>
| Uint16Array<ArrayBuffer>
| Int32Array<ArrayBuffer>
| Uint32Array<ArrayBuffer>
| BigInt64Array<ArrayBuffer>
| BigUint64Array<ArrayBuffer>;
function bytes(n: number): Uint8Array<ArrayBuffer>;
function fillBytes<T extends BufferType>(buffer: T, start?: number, end?: number): T;
}
これを使うことで、例えば六面ダイスの値を返す函数が以下のようにわかりやすく定義できるようになります。
function getRandomDiceRoll() {
return Ramdom.int(1, 6);
}
会議では他にも追加するユーティリティ函数について提案されましたが、スコープが大きくなりすぎているということで別の提案としてスプリットされました。
Stage 0 Random Collection Functions
Random Functions の提案からスプリットされた、配列などのコレクションを扱う函数を提供する提案です。
namespace Random {
function shuffle<Self extends ArrayLike<unknown>>(coll: Self): Self;
function toShuffled<T>(coll: Iterable<T>): Array<T>;
function sample<T>(coll: Iterable<T>, options?: object): T;
function take<T>(coll: Iterable<T>, n: number, options?: object): Array<T>;
function pop<T>(coll: ArrayLike<T> | Set<T>): T;
function pop<K, V>(coll: Map<K, V>): [K, V];
}
これにより Random.shuffle(array)
とするだけで配列をシャッフルできるようになります。なお新しく配列を作る場合は非破壊的な Ramdom.toShuffled(array)
を使うことができます。
またコレクションからランダムに 1 つ値を取り出す Random.sample
函数や、複数値を取り出す Random.take
そして値を 1 つ取り出してそれを元のコレクションから取り除く Random.pop
函数も追加されます。
Stage 0 Random Non-Uniform Distributions
Random Functions の提案からスプリットされた、一様分布でない疑似乱数を扱う函数を提供する提案です。
namespace Random {
normal(mean?: number, stdev?: number): number;
lognormal(mean?: number, stdev?: number): number;
vonmisse(mean?: number, kappa?: number): number;
triangular(lo?: number, hi?: number, mode?: number): number;
exponential(lambda?: number): number;
binomial(n: number, p?: number): number;
geometric(p?: number): number;
hypergeometric(n: number, N: number, K: number): number;
beta(alpha: number, beta: number): number;
gamma(alpha: number, beta: number): number;
pareto(alpha: number): number;
weibull(alpha: number, beta: number): number;
}
Stage 2 Seeded Pseudo-Random Numbers
シード付き疑似乱数を扱う函数、クラスを提供する提案です。もともとは別の提案として進んでいましたが、Random Functions の提案を受けて Random
ネームスペースに入れる方針となりました。
namespace Random {
// ユーザーエージェントにより設定されたシードを元に 0 以上 1 未満の一様乱数を返す
function random(): number;
function seed(): Uint8Array<ArrayBuffer>;
class Seeded {
#state: Uint8Array<ArrayBuffer>;
constructor(seed: Uint8Array<ArrayBuffer>);
static fromSeed(seed: Uint8Array<ArrayBuffer>): Seeded;
static fromFixed(byte: number): Seeded;
static fromState(state: Uint8Array<ArrayBuffer>): Seeded;
random(): number;
seed(): Uint8Array<ArrayBuffer>;
getState(): Uint8Array<ArrayBuffer>;
setState(state: Uint8Array<ArrayBuffer>): this;
}
}
コンストラクタに渡すシードは長さ 32 bytes 以下の Uint8Array
として渡します(長さが 32 bytes 未満の場合は先頭が 0 でパディングされる)。
一方で Random.Seeded.fromSeed
には必ず長さ 32 bytes の Uint8Array
を渡す必要があります。また Random.Seeded.fromFixed
に 0 から 255 までの整数値を渡すことで、1 byte のみから Random.Seeded
を作ることができます。
// 以下はどちらも同じシードから Random.Seeded を作る
const seed = new Uint8Array(32);
seed[31] = 42;
const seeded1 = Random.Seeded.fromSeed(seed);
const seeded2 = Random.Seeded.fromFixed(42);
内部状態 state
を使い回すことで同じ乱数列を取得することができます。
const seeded = Random.Seeded.fromFixed(0);
// いくつか乱数を取得し、内部状態を進める
for (let i = 0; i < 10; ++i) {
seeded.random();
}
const cloned = Random.Seeded.fromState(seeded.getState());
console.log(seeded.random() === cloned.random()); // true
異なるエンジンでも同じ乱数が取り出せるように、ChaCha12 アルゴリズムを用いることが定められています。
なお Random Functions で追加される各ユーティリティ函数が Random.Seeded
にも追加されるようです。
結び
2025年5月の TC39 会議の議事録を読んで、ずっと欲しかった機能だったため勢いでまとめてみました。
ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。
Discussion