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


ссылка на оригинал статьи https://habr.com/ru/articles/825986/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *