Вы должны думать об этом названии? Это вообще возможно? Генератор случайных чисел был проиллюстрирован как пример побочных эффектов. Каждый раз, когда мы дважды вызываем random.nextInt, значение не будет одинаковым. Мы не знаем nextInt, кроме того, мы знаем, что он генерирует случайные значения. Давайте посмотрим на обычный императивный API, основанный на побочных эффектах:

В этой статье мы сначала обсудим, почему нам нужен генератор случайных чисел без побочных эффектов. Затем мы изменим этот API, чтобы он был ссылочно прозрачным. К концу этого вы узнаете, как создать функциональную императивную программу с чистыми состояниями.

Зачем вообще заморачиваться?

Мы знаем, что генератор случайных чисел всегда будет генерировать случайные значения. Однако это становится проблемой, когда мы включаем «случайность» в нашу программу.

Представьте, что вам нужно разработать программу, которая использует случайные числа для выполнения AB-тестирования двух разных функций. Однако вы сталкиваетесь с проблемами при выполнении модульного тестирования. Как вы проводите модульное тестирование этого приложения, поскольку оно создает побочный эффект?

Кроме того, вы можете подумать, что любые ошибки в параллельной среде будет сложно воспроизвести. Поскольку вы не знаете, как воспроизвести проблему, вам будет трудно объяснить своему высшему руководству или руководителю, почему в 2 часа ночи во вторник в вашей системе произошел значительный сбой.

Как вы создаете случайность в своей программе, которую можно легко тестировать и, что наиболее важно, способную надежно воспроизводить ошибки?

Понимание случайности

Прежде чем мы займемся проблемой, мы должны понять, как генерируется «случайность». Вернемся к API обычного генератора случайных чисел из стандартной библиотеки scala:

Мы не знаем, как nextInt реализована функция. Однако мы знаем, что когда мы вызываем random.nextInt, внутри есть некоторое внутреннее состояние, которое обновляется. После обновления он возвращает новое значение. Поскольку обновление страны выполняется как побочный эффект, функция не является ссылочно прозрачной.

Из приведенного выше наблюдения мы можем вывести два факта. Во-первых, в программах не бывает «чистой» случайности. Все они представляют собой функции, которые производят «псевдо» случайный вызов для вызывающей стороны. Причина, по которой вызывающая сторона думает, что это случайно, заключается в том, что они не раскрывают свое внутреннее состояние вызывающей стороне. Он дает звонящему номер, который он не может воспроизвести. Таким образом, это «случайно». Во-вторых, мы можем примерно понять, как реализован nextInt. Когда мы создаем экземпляр числа random, оно предоставляет внутреннее начальное значение генератору случайных чисел. Затем каждый раз, когда мы вызываем nextInt, он будет использовать некоторые алгоритмы, обновлять свое внутреннее состояние и возвращать новое значение.

Покажите мне, как это решить!

Да, мы доберемся! Но я хочу показать вам, как мы можем вывести это чисто функциональное состояние, чтобы вы могли реализовать этот вывод не только для этой конкретной проблемы, но и для других проблем с состоянием внутри.

Мы знаем, что каждый раз, когда мы вызываем генератор случайных чисел, мы можем предоставить его генератору случайных чисел начальное значение. Это начальное значение будет использовать некоторый алгоритм для вычисления от текущего состояния до следующего состояния. Следовательно, чтобы воспроизвести «тот же самый» генератор, нам нужно иметь то же начальное число с тем же состоянием, чтобы воспроизвести то же самое будущее состояние.

Однако в обычном генераторе случайных чисел после генерации следующего состояния предыдущее состояние уничтожается. Это становится трудным для получения того же результата. Поскольку нам дано начальное значение, нам нужно отслеживать, сколько раз вызывается nextInt или nextDouble для воспроизведения одного и того же результата.

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

В примере генератора случайных чисел вместо изменения состояния внутри функции на месте мы возвращаем новое состояние и сгенерированное «случайное» число обратно вызывающей стороне.

Давайте заново создадим генератор случайных чисел. Для этого генератора случайных чисел мы будем использовать утилиту случайных чисел scala, передав начальное значение в scala.util.Random(seed). Затем вычислите следующее начальное значение и верните новый генератор случайных чисел вызывающей стороне.

Мы можем запустить этот генератор случайных чисел, вызвав приведенный ниже пример:

Мы возвращаем новое состояние со старым состоянием без изменений. Мы отделяем вычисление нового состояния от передачи этого нового состояния остальной части программы. Внутренняя память состояний не используется. Мы просто даем вызывающей стороне следующее состояние и позволяем вызывающей стороне иметь полный контроль, чтобы решить, что делать с этим новым состоянием.

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

Я приведу еще один пример для класса с внутренним состоянием, и как вы можете реорганизовать его, чтобы он был чистым.

Каждый раз, когда вы видите класс с таким внутренним состоянием:

Предположим, что put и write каким-то образом изменят состояние map.

Вы можете перевести приведенный выше класс, сделав явным переход из одного состояния в другое, изменив его на это:

Теперь вы заметили, что если вы хотите выполнить последовательность вычислений практичным способом, это будет неудобно и утомительно. Следовательно, мы можем абстрагировать эти вычисления в монаду состояния. Тем не менее, это будет пост для другого дня.

Главный вывод

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

Спасибо, что прочитали! Если вам понравился этот пост, не стесняйтесь подписаться на мою рассылку, чтобы получать уведомления о эссе о карьере в технологиях, интересных ссылках и контенте!

Вы можете подписаться на меня, а также подписаться на Medium, чтобы получать больше подобных сообщений.

Первоначально опубликовано на https://edward-huang.com.