本文へ移動
Hikari 仕様

std:random — 擬似乱数

標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。

import random := "std:random"ジェネレーターを作る 2 つの生成口 (seed / new) を持つ namespace object を random に束縛する。乱数の非決定性と可変状態は、生成した個々の ジェネレーター object の中に閉じ込める。std:fs / std:http/client のような外部リソース I/O ではなく、std:math と同じく 同期 で値を直接返す (Future を返さない)。

std:randomimport すること自体が「このコードは非決定性 (乱数) を使う」という capability を可視化する (index.md)。

import random := "std:random"

g := random.seed(42)      # 再現可能: 同じシードは同じ列
g.int(1, 6)               #> 1..=6 のどれか
g.float()                 #> [0.0, 1.0) のどれか

r := random.new()         # 実行のたびに異なる (種は起動時の時刻由来)
r.bool()                  #> true / false のどちらか

以下の例では import random := "std:random" で束縛したものとして記す。

本書の例は具体の値を書かない。約束しているのは値域 (§3) と、同じシードが同じ列を生むこと (§2) の 2 つであって、seed(42) がどの値から始まるかではない。特定の値を書くと、読者はそれを約束と読み、擬似乱数源を差し替えた途端に黙って嘘になる。

1. モデル — 生成口とジェネレーター

std:random は 2 層からなる。

  • 生成口 (random.seed / random.new) — ジェネレーターを作る。モジュール直下のスロット。
  • ジェネレーター (g) — 乱数を引くメソッド (int / float / float_range / bool / pick / shuffle) を持つ object。乱数を引くたびに内部状態が前進する

ジェネレーターは独立した状態を持つ。同じシードから作った 2 つのジェネレーターは、独立に同じ列を生む。

a := random.seed 7
b := random.seed 7
# 別オブジェクトだが、要素ごとに同じ列である。
[a.int(0, 99), a.int(0, 99)] == [b.int(0, 99), b.int(0, 99)]  #> true

a を引いても b は前進しない。上の例で a から 2 つ引いた後に b から引くと、b は列の先頭から始まる。

ジェネレーター object 自体は closed・immutable (スロットの付け替え・追加は不可) で、std:math の namespace と同じ形をとる。乱数列の前進は object のスロットではなく、各メソッドが共有する内部の擬似乱数源が担う。

ジェネレーターのメソッド(int / float / float_range / bool / pick / shuffle)は Mut を持つ(../language-spec.md §17.9)。呼ぶたびに内部状態が前進し、その書き換えは次の呼びの答えを変えるので呼び出し元から観測できる — フレーム外の可変状態への書き込みにあたるからである。生成口の効果は §2 の表が定める。

2. 生成口 — seed / new

名前 効果 意味
seed seed(n) Mut Int n で初期化した再現可能なジェネレーターを返す
new new() Rand 実行ごとに違う種で初期化した非決定なジェネレーターを返す (§7)
  • seed(n)n は Int。同じ n は必ず同じ乱数列を生む。テスト・再現可能なシミュレーション・確定的なサンプリングに使う。n が Int でなければ §6 のエラー。
  • new() — 引数なし。実行のたびに異なる列を生む非決定ジェネレーターを返す。シードを自分で管理せず「とにかくランダムが欲しい」場合に使う。OS へのブロッキング I/O は伴わず同期で返る (Future ではない)。
random.seed 0  # 確定列 (CI で再現する)
random.new()  # 毎回異なる

呼び出し側で種を明示したいときは random.seed(now_ms) のように書いてもよい。new() はその定型をまとめた便宜口にあたる。

3. 整数・浮動小数 — int / float / float_range

名前 効果 値域
int g.int(lo, hi) Mut 閉区間 [lo, hi] (両端を含む) 引数・戻り値とも Int
float g.float() Mut 半開区間 [0.0, 1.0) 戻り値 Float
float_range g.float_range(lo, hi) Mut 半開区間 [lo, hi) (hi を含まない) 引数・戻り値とも Float
  • g.int(lo, hi)lo 以上 hi 以下の Int を一様に返す。lo == hi なら常に lolo/hi は Int で、lo > hi§6 のエラー。
  • g.float()0.0 以上 1.0 未満の Float を一様に返す。
  • g.float_range(lo, hi)lo 以上 hi 未満の Float を一様に返す。lo/hi は Float で、lo > hi§6 のエラー。lo == hi は常に lo (空区間だが端点を返す)。

整数の int閉区間、浮動小数の float / float_range半開区間なのは型ごとの慣例差で、意図的である (整数はサイコロ int(1, 6) のように両端含みが自然、浮動小数は [0, 1) が標準)。各シグネチャの値域を必ず確認すること。

g := random.seed 1
g.int(1, 6)  #> 一様な 1..=6
g.int(5, 5)  #> 5         (lo == hi)
g.float()  #> [0.0, 1.0)
g.float_range(-1.0, 1.0)  #> [-1.0, 1.0)
g.int(6, 1)  # error: int: lo must not be greater than hi (§6)
g.int(1.0, 6.0)  # error: int: arguments must be Int (§6)

4. 真偽・選択 — bool / pick

名前 効果 意味
bool g.bool() Mut true / false を等確率 (各 1/2) で返す
pick g.pick(xs) Mut List xs から一要素を一様に選び Option で返す
  • g.bool() — コイントス。引数なし。
  • g.pick(xs)xs の要素を等確率で 1 つ選ぶ。
    • xs が空 List [] のときは None (../language-spec.md の「不在は Option」、../prelude.md §6.6xs.min() / xs.max() と同じ規律)。
    • 非空なら Some(要素)。要素の型は問わない (数値でなくてよい)。
    • xs が List でなければ §6 のエラー。
g := random.seed(2)
g.bool()                       #> true
match g.pick(["a", "b", "c"]) {
  Some(x) => print(x)  # 選ばれた要素
  None => print("empty")
}
g.pick([])                     #> None

5. シャッフル — shuffle

名前 効果 意味
shuffle g.shuffle(xs) Mut List xs を一様にシャッフルした新しい List を返す
  • g.shuffle(xs)xs の要素を一様ランダムに並べ替えた新しい List を返す (Fisher–Yates)。元の xs は変更しない (Hikari の値は不変、../language-spec.md)。
  • 空 List [][] を、1 要素は同じ並びを返す。
  • xs が List でなければ §6 のエラー。
g := random.seed 3
xs := [1, 2, 3, 4, 5]
g.shuffle xs  #> [1, 2, 3, 4, 5] の置換 (一様。並びはシードで決まる)
xs  #> [1, 2, 3, 4, 5]  (元は不変)

pick が 1 要素を選ぶのに対し、shuffle は全体を並べ替える。xs から重複なく k 個を取りたいときは g.shuffle(xs) の先頭 k 個を使う。

6. エラーモデル

std:random2 種の結果を使い分ける。

  1. Option を返す: pick のみ。空 List で None、非空で Some(要素) (§4)。
  1. Error (バグ層、../language-spec.md §16): 型が合わない呼び出し区間の不整合。回復可能エラーではなく呼び出し位置で停止する。math.md §6json.md §5 の引数型エラーと同じ規律:
    • seed に非 Int を渡す
    • int に非 Int を渡す / float_range に非 Float を渡す
    • int / float_rangelo > hi
    • pick / shuffle に非 List を渡す
    • seednintlo/hi が実装が受理する範囲 (int64) を超える

std:math の「定義域外は NaN を返し続行」のような第 3 の層は持たない (乱数に定義域外の概念がないため)。値域外の区間 (lo > hi) は数学エラーではなく呼び出しミスとして停止する。

7. 再現性と乱数品質

  • 再現性: random.seed(n) は同じ n に対し同じ列を生む。これはテスト・確定的シミュレーションのための保証で、ジェネレーターの内部アルゴリズムは固定する。random.new() で作ったジェネレーターは非決定で、列を再現できない。
  • 品質: std:random統計的な擬似乱数であり、暗号用途には使わない。キー・トークン・パスワード等の生成には crypto/random.mdcrand.bytes(n) を使うこと。
  • new() の種はホストのエントロピー源ではない。 起動時の時刻とこのフロー内で作ったジェネレーターの個数から取るので、同じ時刻に始まった 2 つのプロセスは同じ列を引きうる。統計的な用途には足りるが、予測不能性は主張しない。予測されて困る値をここから作らないこと。
  • 各ジェネレーターは独立した状態を持ち、複数のジェネレーターを並行に使っても互いの列に干渉しない。