コード
// 関数インターフェイスのための型関数定義
type Append<T, U extends unknown[]> = ((x: T, ...y: U) => void) extends (...x: infer V) => void
? V
: never;
declare type FixedLengthTuple<T, L extends number, R extends any[] = []> = {
0: R;
1: FixedLengthTuple<T, L, Append<T, R>>;
}[R["length"] extends L ? 0 : 1];
// 本題の関数定義
type Options<T, L extends number> = {
list: readonly T[];
length: L;
};
function sampleWithoutReplacement<T, L extends number>({
list,
length,
}: Options<T, L>): FixedLengthTuple<T, L> {
const copiedList = list.slice();
const result = [];
for (let i = 0; i < length; i++) {
const index = Math.floor(Math.random() * copiedList.length);
result.push(...copiedList.splice(index, 1));
}
return result as any;
}
解説
型関数について
Append<T, U extends unknown[]>
は配列型の先頭要素に新たな型を追加する型関数。(参考はこちら)
FixedLengthTuple<T, L extends number, R extends any[] = []>
は指定した型の固定長タプル型を生成する型関数。(参考はこちら)
本題の関数について
引数のlist
のコピーを取ります。(後に配列に対して破壊的メソッドを使用するため)
結果を格納しておく空の配列result
を宣言しておきます。
length
で指定された回数だけfor
文を回します。
for
文の中では、まずMath.random()
を利用して配列のindex
をランダムに決めます。続いて、Array.prototype.splice()
を使用して配列の指定箇所から要素を抜き出します。
Array.prototype.splice()
が破壊的メソッドであることを利用して「非復元性」を表現しています。
結果を格納しているresult
は固定長であることをコンパイラが認識しないので、as any
でTypeScriptコンパイラを黙らせます。(プログラマーの勝利)
動作確認
こんなのでいいのかわかりませんが、結果にばらつきはなさそうです。(統計的検定までは勘弁)
const test: any = {};
for (let i = 0; i < 100000; i++) {
const numbers = sampleWithoutReplacement({ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], length: 1 })
numbers.forEach(number => {
test[number] = test[number] ? test[number] + 1 : 1;
})
}
console.log(test);
//{ "0": 10076, "1": 10154, "2": 9928, "3": 9998, "4": 9963, "5": 10068, "6": 9863, "7": 10024, "8": 9953, "9": 9973 }