Random Unification
01 何が問題だったのか
Swift には標準ライブラリとしての乱数 API が長らく存在せず、乱数がほしいときは、プラットフォームごとに異なる C のライブラリ関数を呼び出すしかありませんでした。具体的には、Darwin では arc4random(3) 系、Linux では POSIX の random(3) や C 標準ライブラリの rand(3) がよく使われ、同じ Swift コードでも OS ごとに別実装を書き分ける必要がありました。
実際に書かれがちだったのは、次のようなコードです。
import Foundation
#if os(Linux)
srandom(UInt32(time(nil)))
#endif
func randomNumber() -> Int {
#if !os(Linux)
return Int(arc4random()) // UInt32 の範囲しか使えず Int の広さを活かせない
#else
return random() // Linux では事前に seed が必要
#endif
}
func random(from start: Int, to end: Int) -> Int {
#if !os(Linux)
var random = Int(arc4random_uniform(UInt32(end - start)))
#else
// 剰余バイアスが乗る
var random = random() % (end - start)
#endif
random += start
return random
}
この状況には、ライブラリが整っていないという以上に、いくつかの深刻な問題がありました。
- プラットフォーム分岐が必要: 「乱数が 1 つほしい」というごく基本的な要求のために、ユーザー側で
#if os(Linux)のような分岐を書かされます。 - 安全でない実装を使いがち: Linux の
random()やrand()は暗号学的に安全ではなく、Darwin のarc4randomも古い macOS/iOS では RC4 ベースで弱い実装でした。経験の浅い開発者ほど、こうした API をそのまま本番コードに持ち込みがちです。 - 剰余バイアス: 「
% (end - start)」で範囲内の値を得るイディオムは広く使われますが、上限値付近の値の出現確率が他より低くなる剰余バイアスを生みます。 - 範囲を活かしきれない:
arc4random()はUInt32しか返さないので、Intの全範囲の乱数がほしい場合には情報が足りません。 - 提供元がわかりにくい: これらの関数は
FoundationやUIKit/AppKitをインポートすると使えるようになりますが、実際の定義元は内部的に取り込まれた Darwin/Glibc であり、Foundation が提供していると誤解されがちです。
要するに、「プラットフォームに依存せず、デフォルトで安全で、剰余バイアスなどの罠を避けた、素直な乱数 API」が標準ライブラリに欠けていたのです。
02 どのように解決されるのか
標準ライブラリに、統一された乱数 API と、そのデフォルト実装として暗号学的に安全な乱数生成器を追加します。これにより、プラットフォーム分岐や外部ライブラリなしで、安全な乱数を手軽に得られるようになります。
乱数生成器のプロトコルとデフォルト実装
乱数生成器(RNG)は RandomNumberGenerator プロトコルで抽象化されます。独自の RNG を実装したい場合は、このプロトコルに適合させて next() を定義するだけで、標準ライブラリの乱数 API に差し込めます。
public protocol RandomNumberGenerator {
mutating func next() -> UInt64
}
標準ライブラリはデフォルト RNG として SystemRandomNumberGenerator を提供します。これはプラットフォームごとに最適な暗号学的に安全な乱数源を内部で利用し、スレッドセーフに動作することを目指した実装です(具体的な実装はプラットフォームベンダが決定します)。もし乱数の取得に失敗した場合は、エラーを返すのではなく致命的エラーでプログラムを終了させます。これは、たとえば /dev/urandom が読めないといった状況が通常のアプリでは起こり得ず、万一起きたときは続行すべきではないという判断に基づきます。結果として、呼び出し側では戻り値を Optional でほどいたり、try を書いたりする必要がありません。
数値型・Bool 型への random(in:) と random()
FixedWidthInteger、BinaryFloatingPoint、Bool に、範囲や型を指定して乱数を得る static メソッドが追加されます。どのメソッドにも、引数なしで呼ぶとデフォルト RNG を、using: で渡すと任意の RNG を使う 2 種類のオーバーロードが用意されます。
// 整数: 半開区間・閉区間のどちらも使える
let dice = Int.random(in: 1 ... 6)
let index = Int.random(in: 0 ..< array.count)
// 浮動小数点数
let unit = Double.random(in: 0 ..< 1)
let angle = Double.random(in: 0 ... .pi)
// Bool
let coin = Bool.random()
// 独自 RNG を差し込む
var rng = MyCustomRNG()
let value = UInt.random(in: .min ... .max, using: &rng)
範囲を受け取る設計になっているのは、% n のような剰余ベースのイディオムを防ぎ、剰余バイアスのない一様分布を保証するためです。空の範囲を渡した場合はプログラムがトラップします。呼び出し側で空になり得る場合は、事前に if/guard で弾いておく必要があります。
Collection からランダムに要素を取り出す
Collection には randomElement() が追加されます。空のコレクションを考慮して戻り値は Optional です(first・last・min()・max() と同じ流儀です)。
let greetings = ["hey", "hi", "hello", "hola"]
print(greetings.randomElement()!) // 例: "hola"
var rng = MyCustomRNG()
let picked = greetings.randomElement(using: &rng)
なお、Int.min...Int.max のような Int で表現しきれないほど要素数の多いコレクションでは randomElement() はトラップし得ます。こうした用途では代わりに Int.random(in:) を使います。
シャッフル API
乱数 API が入るのに合わせて、Sequence の shuffled() と MutableCollection の shuffle() も追加されます。内部アルゴリズムは Fisher-Yates です。
var greetings = ["hey", "hi", "hello", "hola"]
greetings.shuffle()
// 例: ["hola", "hello", "hey", "hi"]
let numbers = 0 ..< 5
print(numbers.shuffled()) // 例: [1, 3, 0, 4, 2]
// RNG を差し替えることもできる
var rng = MyCustomRNG()
let reordered = numbers.shuffled(using: &rng)
デフォルトが安全であること
この API の大きな意図のひとつは、デフォルトで安全な乱数を使わせる ことです。Int.random(in: 1 ... 6) のように何気なく書いたコードが、暗号学的に安全な乱数源の上で動くことが保証されるため、経験の浅いコードでもうっかり脆弱な乱数を本番に持ち込んでしまう事態を避けられます。決定論的な疑似乱数や特定分布が必要な場合は、RandomNumberGenerator に適合した独自型を作り、using: 付きのオーバーロードに差し込む、という明示的な選択肢として切り分けられます。