[Перевод] Изучение случайности в JavaScript

В моем вчерашнем посте о создании утилиты цветовой палитры в 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(), чтобы посмотреть, будет ли один из методов существенно отличаться по ощущениям. Каждая попытка будет содержать случайно сгенерированный элемент и случайно сгенерированный набор целых чисел. Затем я воспользуюсь своей (глубоко ошибочной) человеческой интуицией, чтобы понять, выглядит ли один из методов «лучше» другого.

Метод 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() будет использоваться:




	
	
	



	

Exploring Randomness In JavaScript

Math Module

Duration:

Crypto Module

Duration: ms

Как видите, каждый компонент 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 ) ) );

	}

}

Когда мы запускаем этот пример, мы получаем следующий результат:

8091b374f993b82a04e518413b2fe5ab.png

Как я уже говорил выше, случайность с человеческой точки зрения очень размыта. Она больше связана с ощущениями, чем с математическими вероятностями. Например, вероятность того, что подряд появятся два одинаковых значения, равна вероятности того, что подряд появятся два разных значения. Но для человека это ощущается по-другому.

Тем не менее, если сравнить эти визуализации случайной генерации, ни одна из них не кажется существенно отличающейся с точки зрения распределения. Конечно, модуль Crypto значительно медленнее (половина из этого — затраты на выделение ресурсов под TypedArray). Но с точки зрения «ощущений» ни один из них не является лучше другого.

Скажу лишь, что при использовании генерации в утилите цветовой палитры мне, вероятно, не было необходимости использовать модуль Crypto — возможно, стоило остановиться на Math. Это гораздо быстрее и ощущается таким же случайным. Я буду использовать модуль Crypto для работы с криптографией на стороне клиента (чего мне пока не приходилось делать).

Habrahabr.ru прочитано 3401 раз