В моем посте о создании утилиты цветовой палитры в Alpine.js случайность играла большую роль: каждый образец генерировался как композиция случайно выбранных значений Hue (0..360)
, Saturation (0..100)
и Lightness (0..100)
. Когда я создавал эту демонстрацию, я наткнулся на Web Crypto API. Обычно при генерации случайных значений я использую метод Math.random()
, но в документации MDN упоминается, что Crypto.getRandomValues()
более безопасен. В итоге я решил попробовать Crypto
(с фоллбэком на модуль Math
по мере необходимости). Но это заставило меня задуматься, действительно ли "более безопасный" означает "более случайный" для моего варианта использования.
Посмотреть пример в моем проекте JavaScript Demos на GitHub.
Посмотреть код в моем проекте JavaScript Demos на GitHub.
Случайность, с точки зрения безопасности, имеет значение. Я не специалист по безопасности, но, насколько я понимаю, генератор псевдослучайных чисел (ГПСЧ) считается "безопасным" в том случае, когда последовательность чисел, которую он произведет или уже произвел, не может быть вычислена злоумышленником.
Когда речь идет о "генераторах случайных цветов", таких, как моя утилита для создания цветовой палитры, понятие "случайности" гораздо более расплывчато. В моем случае генерация цвета настолько случайна, насколько это «ощущается» пользователем. Другими словами, эффективность случайности является частью пользовательского опыта (UX).
С этой целью я хочу попробовать сгенерировать несколько случайных визуальных элементов, используя как Math.random()
, так и crypto.getRandomValues()
, чтобы посмотреть, будет ли один из методов существенно отличаться по ощущениям. Каждая попытка будет содержать случайно сгенерированный элемент <canvas>
и случайно сгенерированный набор целых чисел. Затем я воспользуюсь своей (глубоко ошибочной) человеческой интуицией, чтобы понять, выглядит ли один из методов "лучше" другого.
Метод Math.random()
работает, возвращая десятичное значение от 0
(включительно) до 1
(исключительно). Это можно использовать для генерации случайных целых чисел, взяв результат случайности и умножив его на диапазон возможных значений.
Другими словами, если Math.random()
вернет 0.25
, вы выберете значение, которое ближе всего к 25% в заданном диапазоне минимума-максимума. А если Math.random()
вернет 0.97
, вы выберете значение, которое ближе всего к 97% в заданном диапазоне минимума-максимума.
Метод crypto.getRandomValues()
работает совсем по-другому. Вместо того чтобы вернуть вам единственное значение, он ожидает принять TypedArray с заранее выделенным размером (длиной). Затем метод .getRandomValues()
заполняет этот массив случайными значениями, ограниченными минимумом/максимумом, которые может хранить данный тип.
Чтобы облегчить это исследование, я хочу, чтобы оба подхода работали примерно одинаково. Поэтому вместо того, чтобы иметь дело с десятичными числами в одном алгоритме и целыми числами в другом, я приведу результаты алгоритмов к десятичным числам. Это означает, что я должен превратить value
, возвращаемое .getRandomValues()
, в десятичное число (0..1
):
value / ( maxValue + 1 )
Я инкапсулирую эту разницу в два метода, randFloatWithMath()
и randFloatWithCrypto()
:
/**
* С помощью модуля Math я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithMath() {
return Math.random();
}
/**
* С помощью модуля Crypto я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithCrypto() {
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
return ( randomInt / ( maxInt + 1 ) );
}
Имея эти два метода, я могу присвоить один из них переменной randFloat()
, которая может быть использована для генерации случайных значений в заданном диапазоне, используя любой из алгоритмов:
/**
* Я генерирую случайное целое число между заданными min и max, включительно.
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
Теперь перейдем к созданию экспериментов. Пользовательский интерфейс небольшой и работает на Alpine.js. В каждом эксперименте используется один и тот же компонент Alpine.js, но его конструктор получает аргумент, который определяет, какая реализация randFloat()
будет использоваться:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1>
<!-- Изучение случайности в JavaScript --> Exploring Randomness In JavaScript
</h1>
<div class="side-by-side">
<section x-data="Explore( 'math' )">
<h2>
<!-- Модуль Math --> Math Module
</h2>
<!-- Очень большое количество случайных координат {X,Y}. -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- Небольшое количество случайных значений координат. -->
<p x-ref="list"></p>
<p>
<!-- Длительность --> Duration: <span x-text="duration"></span>
</p>
</section>
<section x-data="Explore( 'crypto' )">
<h2>
<!-- Модуль Crypto --> Crypto Module
</h2>
<!-- Очень большое количество случайных координат {X,Y}. -->
<canvas
x-ref="canvas"
width="320"
height="320">
</canvas>
<!-- Небольшое количество случайных значений координат. -->
<p x-ref="list"></p>
<p>
<!-- Длительность --> Duration: <span x-text="duration"></span>ms
</p>
</section>
</div>
<script type="text/javascript" src="./main.js" defer></script>
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
</body>
</html>
Как видите, каждый компонент x-data="Explore"
содержит два x-ref
: canvas
и list
. Когда компонент инициализируется, он заполнит эти два x-ref
случайными значениями с помощью методов fillCanvas()
и fillList()
соответственно.
Вот мой компонент JavaScript / Alpine.js:
/**
* С помощью модуля Math я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithMath() {
return Math.random();
}
/**
* С помощью модуля Crypto я возвращаю случайное число с плавающей запятой в диапазоне от 0 (включительно) до 1 (исключительно).
*/
function randFloatWithCrypto() {
// Этот метод работает, заполняя массив случайными значениями заданного типа.
// В нашем случае нам нужно только одно случайное значение, поэтому мы передадим массив
// длиной 1.
// --
// Примечание: Для повышения производительности мы можем кэшировать типизированный массив и просто передавать
// одну и ту же ссылку (это улучшает производительность вдвое). Но мы исследуем
// случайность, а не производительность.
var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
var maxInt = 4294967295;
// В отличие от Math.random(), crypto генерирует нам целое число. Чтобы подставить его
// в то же математическое уравнение, мы должны преобразовать целое число в десятичное,
// чтобы получить такое же случайное значение.
return ( randomInt / ( maxInt + 1 ) );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
function Explore( algorithm ) {
// Каждому компоненту Alpine.js назначается своя стратегия генерации случайных
// чисел с плавающей запятой (0..1). В остальном компоненты ведут себя
// одинаково.
var randFloat = ( algorithm === "math" )
? randFloatWithMath
: randFloatWithCrypto
;
return {
duration: 0,
// Публичные методы.
init: init,
// Приватные методы.
fillCanvas: fillCanvas,
fillList: fillList,
randRange: randRange
}
// ---
// ПУБЛИЧНЫЕ МЕТОДЫ.
// ---
/**
* Я инициализирую компонент Alpine.js.
*/
function init() {
var startedAt = Date.now();
this.fillCanvas();
this.fillList();
this.duration = ( Date.now() - startedAt );
}
// ---
// ПРИВАТНЫЕ МЕТОДЫ.
// ---
/**
* Я заполняю canvas случайными пикселями {X,Y}.
*/
function fillCanvas() {
var pixelCount = 200000;
var canvas = this.$refs.canvas;
var width = canvas.width;
var height = canvas.height;
var context = canvas.getContext( "2d" );
context.fillStyle = "deeppink";
for ( var i = 0 ; i < pixelCount ; i++ ) {
var x = this.randRange( 0, width );
var y = this.randRange( 0, height );
// По мере добавления новых пикселей изменяем их непрозрачность.
// Я надеялся, что это поможет показать потенциальную кластеризацию значений.
context.globalAlpha = ( i / pixelCount );
context.fillRect( x, y, 1, 1 );
}
}
/**
* Я заполняю список случайными значениями от 0 до 9.
*/
function fillList() {
var list = this.$refs.list;
var valueCount = 105;
var values = [];
for ( var i = 0 ; i < valueCount ; i++ ) {
values.push( this.randRange( 0, 9 ) );
}
list.textContent = values.join( " " );
}
/**
* Я генерирую случайное целое число между заданными min и max, включительно.
*/
function randRange( min, max ) {
return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}
}
Когда мы запускаем этот пример, мы получаем следующий результат:
Как я уже говорил выше, случайность с человеческой точки зрения очень размыта. Она больше связана с ощущениями, чем с математическими вероятностями. Например, вероятность того, что в одном ряду появятся два одинаковых значения подряд, равна вероятности того, что в одном ряду появятся два разных значения подряд. Но для человека это ощущается по-другому.
Тем не менее, если сравнить эти визуализации случайной генерации, ни одна из них не кажется существенно отличающейся с точки зрения распределения. Конечно, модуль Crypto
значительно медленнее (половина из этого - затраты на выделение ресурсов под TypedArray). Но с точки зрения "ощущений" ни один из них не является лучше другого.
Скажу лишь, что при использовании генерации в утилите цветовой палитры мне, вероятно, не было необходимости использовать модуль Crypto
- возможно, стоило остановиться на Math
. Это гораздо быстрее и ощущается таким же случайным. Я буду использовать модуль Crypto
для работы с криптографией на стороне клиента (чего мне пока не приходилось делать).
Комментарии (14)
Djaler
03.07.2024 20:11+5вероятность того, что подряд появятся два одинаковых значения, равна вероятности того, что подряд появятся два разных значения
Вероятность первого - 1/10, вероятность второго - 9/10. Или я что-то не так понимаю?
zhenkuzne Автор
03.07.2024 20:11Например, вероятность того, что в одном ряду появятся два одинаковых значения подряд, равна вероятности того, что в одном ряду появятся два разных значения подряд.
Спасибо за замечание, я думаю, так правильнее.
zorn-v100500
03.07.2024 20:11Т.е. вероятность получить 88 точно такая же как и у 80,81,82,83,84,85,86,87,89
Вы ничего не путаете ?zhenkuzne Автор
03.07.2024 20:11Имеется в виду не это, а то, что в ряду (который на картинке изображен) вероятность того, что появится хотя бы одна пара одинаковых значений такая же, как появление хотя бы одной пара разных значений.
Вот выдержка с оригинала:
For example, having two of the same values generated in a row is the same exact probability of having any two values generated in a row.
zorn-v100500
03.07.2024 20:11Вы наверное путаете с орел/решка. Когда 50/50 (за которую заминусили), тогда да, вероятность одинаковая. Но вы предлагаете 1/10 вероятность, называя ее 50/50 )
ALapinskas
03.07.2024 20:11Случайность, с точки зрения безопасности, имеет значение. Я не специалист по безопасности, но, насколько я понимаю, генератор псевдослучайных чисел (ГПСЧ) считается "безопасным" в том случае, когда последовательность чисел, которую он произведет или уже произвел, не может быть вычислена злоумышленником.
По-моему, "безопасный" алгоритм должен работать от источника энтропии, все остальные алгоритмы могут быть вычислены, т.е. они псевдослучайны.
adminNiochen
03.07.2024 20:11+4На дворе 2024 год, кто-то до сих пор использует var и пишет function ObjectCreator вместо class...
khe404
03.07.2024 20:11Какая сложная и одновременно простая штука -- случайное число.
Когда нужно чтобы число "казалось" случайным достаточно использовать некий математический алгоритм, например ЛКМ https://ru.wikipedia.org/wiki/Линейный_конгруэнтный_метод
А когда нужно чтобы число было "заранее непредсказуемым" нужно использовать информацию взятую вне системы, иначе условны недоброжелатель сможет повторить вычисления.
SUNsung
Отличная статья о том как "проверять" рандом
Только я бы еще добавил цветовую дифференциацию и запускал бы параллельно в нескольких потоках (которые отличаются цветом). За js не скажу но в некоторых языках программирования как раз асинхрон показывает кривизну рандома. Может оказатся что для одновременных запросов одно и то же число выдает.
KasperGreen
А я думал нужно запустить рандом множество раз и убедиться, что с большим количеством попыток, становится равномернее распределение цифр.
А тут оказывается достаточно было один раз запустить и внешне посмотреть. Так сказать проверка генератора случайных чисел на глазок.
Таким грешат генераторы черпающие энтропию в системном времени.
alienator
В этом «равномернее» убедиться тоже очень непросто, как и выбрать из разных видов «равномерности».
Например, можно считать ГСЧ очень хорошим, а потом рассмотреть выдаваемые числа как координаты точек в пятимерном пространстве, и окажется, что все точки легли на одну плоскость.
Но придумывать с нуля ничего не надо, есть разнообразные тесты ГСЧ, начиная с хорошо известных Diehard tests.