PHP8.2で乱数が大改善されましたが、早くもPHP8.3で幾つかの機能が追加されることが決まりました。
以下は該当のRFC、Randomizer Additionsの紹介です。
PHP RFC: Randomizer Additions
Introduction
このRFCでは、ユーザランドでの実装が困難であったり面倒であったりする、幾つかの有用な機能を\Random\Randomizerに追加することを提案します。
識別子、バウチャーコード、整数範囲を超える数値文字列を作るといった用途で、特定の文字を含むランダムな文字列を生成することはよくあります。
この操作をユーザランドで実装するには、ループ中で入力文字列からランダムなオフセットを取る必要があり、非常に単純な内容であるにもかかわらず何行ものコードが必要になります。
また文字列長から1を引くのを忘れるなど、微妙なバグも発生しがちです。
ランダムな浮動小数値の生成は、たとえば一定確率でbool値を出したいといった場合にも有用です。
これをユーザランドで行うことは、一見簡単そうに見えて実は面倒です。
Randomizer::getInt()
で取得した値を割るというよくある実装は、丸め誤差や浮動小数の偏りなどにより、わりと正しくないことがよくあります。
Proposal
\Random\Randomizer
に3つのメソッド、およびひとつのenumを追加します。
namespace Random;
final class Randomizer {
// […]
public function getBytesFromString(string $string, int $length): string {}
public function nextFloat(): float {}
public function getFloat(
float $min,
float $max,
IntervalBoundary $boundary = IntervalBoundary::ClosedOpen
): float {}
}
enum IntervalBoundary {
case ClosedOpen;
case ClosedClosed;
case OpenClosed;
case OpenOpen;
}
getBytesFromString
与えられた文字列から、ランダムな文字列を生成します。
第一引数$string
は選択対象の文字列。
同じ文字が複数あった場合、その文字が選択される確率が増える。
全ての文字が1回しか登場しない場合、選択される確率は一様である。
第二引数$length
は返り値の長さ。
Example
$randomizer = new \Random\Randomizer();
// ランダムなドメイン名
var_dump(sprintf(
"%s.example.com",
$randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16)
)); // string(28) "xfhnr0z6ok5fdlbz.example.com"
// 多要素認証の認証コード
var_dump(
implode('-', str_split($randomizer->getBytesFromString('0123456789', 20), 5))
); // string(23) "09898-46592-79230-33336"
// 小数
var_dump(sprintf(
'0.%s',
$randomizer->getBytesFromString('0123456789', 30)
)); // string(30) "0.217312509790167227890877670844"
// aが75%、bが25%で出現するランダム文字列
var_dump(
$randomizer->getBytesFromString('aaab', 16)
); // string(16) "baabaaaaaaababaa"
// DNA
var_dump(
$randomizer->getBytesFromString('ACGT', 30)
); // string(30) "CGTAGATCGTTCTGATAGAAGCTAACGGTT"
小数を作る場合、最後の桁に0が入る可能性があることに注意してください。
getFloat()
引数$min
と$max
の間の小数を返します。
区間の端が開いているか閉じているかは、引数$boundary
に依存します。
デフォルトは半開区間[$min, $max)
であり、$min
を含みますが$max
は含まれません。
返される値は、設定された区間内に均等分布しています。
均等分布しているとは、各区間の値になる割合が、同じ幅の他の区間になる値と同じ割合ということです。
たとえばgetFloat(0, 1, IntervalBoundary::ClosedOpen)
を呼び出したときの返り値が0.5未満になる確率は、0.5以上になる確率と同じです。
同じく、0.1未満になる確率は10%であり、0.9以上になる確率と同じです。
使用したアルゴリズムはDrawing Random Floating-Point Numbers from an Intervalという論文で公開されているγ-section
というものです。
第一引数$min
は最小値。
第二引数$max
は最大値。
第三引数$boundary
は返り値における境界の扱い。
・デフォルトは\Random\IntervalBoundary::ClosedOpen
で[$min, $max)
・ClosedClosed
は[$min, $max]
・ClosedOpen
は($min, $max]
・OpenOpen
は($min, $max)
Example
$randomizer = new \Random\Randomizer();
// 経緯度
// 緯度は90/-90どちらも可
// 経度は180はあるけど-180はない
var_dump(sprintf(
"Lat: %+.6f Lng: %+.6f",
$randomizer->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed),
$randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed),
)); // string(32) "Lat: -51.742529 Lng: +135.396328"
nextFloat()
このメソッドは->getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen)
と同じです。
[0, 1)
を取得したいというありがちなケースを、より高速に処理します。
Example
$randomizer = new \Random\Randomizer();
// 50%の確率
var_dump(
$randomizer->nextFloat() < 0.5
); // bool(true)
// 10%の確率
var_dump(
$randomizer->nextFloat() < 0.1
); // bool(false)
Backward Incompatible Changes
クラス名\Random\IntervalBoundary
が使用できなくなります。
名前空間\Random
はPHPの乱数で使用されているものであり、またGitHubでも同名クラスはヒットしないので、実影響はないでしょう。
Proposed PHP Version(s)
PHP8.x
RFC Impact
SAPIや拡張モジュールへの影響、定数やphp.iniの変更はありません。
Proposed Voting Choices
getBytesFromString()
は賛成18反対0、getFloat()/nextFloat()
は賛成16反対1の賛成多数で可決されました。
PHP8.3で導入されます。
Patches and Tests
getBytesFromString()の実装
getFloat()/nextFloat()の実装
Implementation
https://github.com/php/php-src/commit/ac3ecd03af009d433d4b75d570b3b0f0a3fc0ff7
https://github.com/php/php-src/commit/f9a1a903805a0c260c97bcc8bf2c14f2dd76ca76
References
感想
ランダム文字列って何気に面倒なんですよね。
よくある例はstr_shuffle
ですが、これは暗号学的に安全でないのであまり適切ではありません。
random_bytesは安全ですが、返り値がバイナリなので[0-9A-Za-z]
に限定したいんだなんてときは使い辛いです。
そんなわけで、実用的に便利なメソッドが追加されました。
暗号学的に安全なパスワードを作りたいなんて要求が、今後は容易に実装可能です。
ちなみにこのRFCにはRandom Extension 5xとRandom Extension Improvementを作った人は関わっていないみたいで、主にメーリングリストでそれらのRFCに突っ込みを入れていた人が作っていました。