🎲

疑似乱数のユーティリティ函数を提供する 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 に疑似乱数のユーティリティ函数(特に配列のシャッフル)を入れることが望まれてきましたが、長いこと達成されませんでした。

https://esdiscuss.org/topic/proposal-array-prototype-shuffle-fisher-yates

Stage 1 Random Functions

2025年5月の TC39 会議にて、新たにグローバルに Random ネームスペースを定義し、いくつかのユーティリティ函数を提供する提案が出され Stage 1 となりました。

https://docs.google.com/presentation/d/1HXjj3VNjNIvb-LBNLFiepVHZiuSNmtoTqGObuBVGXBQ/edit?slide=id.p#slide=id.p

https://github.com/tc39/proposal-random-functions

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 の提案からスプリットされた、配列などのコレクションを扱う函数を提供する提案です。

https://github.com/tabatkins/proposal-random-collection-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 の提案からスプリットされた、一様分布でない疑似乱数を扱う函数を提供する提案です。

https://github.com/tabatkins/proposal-random-distributions

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 ネームスペースに入れる方針となりました。

https://github.com/tc39/proposal-seeded-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 アルゴリズムを用いることが定められています。

https://github.com/tc39/proposal-seeded-random/issues/19

なお Random Functions で追加される各ユーティリティ函数が Random.Seeded にも追加されるようです。

結び

2025年5月の TC39 会議の議事録を読んで、ずっと欲しかった機能だったため勢いでまとめてみました。

https://scrapbox.io/petamoriken/2025-05_の_TC39_meeting

ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。

Discussion