Понимание случайной монады в Scala

Это продолжение моей предыдущей question

Тревис Браун указал, что java.util.Random является побочным эффектом и предложил случайную монаду Rng , чтобы сделать код чисто функциональным. Теперь я пытаюсь создать упрощенную случайную монаду, чтобы понять, как она работает.

Имеет ли смысл? Как вы могли бы исправить/улучшить объяснение ниже?

Случайный генератор

Сначала мы плагируем случайную производящую функцию из java.util.Random

 // do some bit magic to generate a new random "seed" from the given "seed" 
 // and return both the new "seed" and a random value based on it

 def next(seed: Long, bits: Int): (Long, Int) = ...

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

Случайная точка

Теперь напишите функцию для генерации случайной точки в единичном квадрате.

Предположим, что у нас есть функция для генерации случайного двойника в диапазоне [0, 1]

 def randomDouble(seed: Long): (Long, Double) = ... // some bit magic

Теперь мы можем написать функцию для генерации случайной точки.

def randomPoint(seed: Long): (Long, (Double, Double)) = {
   val (seed1, x) = randomDouble(seed)
   val (seed2, y) = randomDouble(seed1)
   (seed2, (x, y)) 
}

До сих пор такие хорошие и оба randomDouble и randomPoint чисты. Единственная проблема заключается в том, что мы создаем randomDouble для сборки randomPoint ad hoc. У нас нет общего инструмента для создания функций, дающих случайные значения.

Monad Random

Теперь мы определим общий инструмент для создания функций, дающих случайные значения. Во-первых, мы обобщаем тип randomDouble:

type Random[A] = Long => (Long, A) // generate a random value of type A

а затем создайте вокруг него класс-оболочку.

class Random[A](run: Long => (Long, A))

Нам нужна оболочка для определения методов flatMap (как bind в Haskell) и map, используемых для понимания.

class Random[A](run: Long => (Long, A)) {
  def apply(seed: Long) = run(seed)  

  def flatMap[B](f: A => Random[B]): Random[B] =
    new Random({seed: Long => val (seed1, a) = run(seed); f(a)(seed1)})

  def map[B](f: A => B): Random[B] =
    new Random({seed: Long = val (seed1, a) = run(seed); (seed1, f(a))})
}  

Теперь мы добавляем factory -функцию для создания тривиального Random[A] (который, кстати, является абсолютно детерминированным, а не "случайным" ). Это функция возврата (как возвращение в Haskell).

def certain[A](a: A) = new Random({seed: Long => (seed, a)})

Random[A] - это вычисление, дающее случайное значение типа A. Методы flatMap, map и функция unit служат для составления простых вычислений для построения более сложных. Например, мы создадим два Random[Double] для сборки Random[(Double, Double)].

Монадическая случайная точка

Теперь, когда у нас есть монада, мы готовы пересмотреть randomPoint и randomDouble. Теперь мы определяем их по-разному как функции, дающие Random[Double] и Random[(Double, Double)]

def randomDouble(): Random[Double] = new Random({seed: Long => ... })
def randomPoint(): Random[(Double, Double)] =
  randomDouble().flatMap(x => randomDouble().flatMap(y => certain(x, y))

Эта реализация лучше предыдущей, поскольку она использует общий инструмент (flatMap и certain) для составления двух вызовов Random[Double] и сборки Random[(Double, Double)].

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

Расчет Монте-Карло Pi

Теперь мы можем использовать map для проверки, является ли случайная точка в круге:

def randomCircleTest(): Random[Boolean] = 
  randomPoint().map {case (x, y) => x * x + y * y <= 1} 

Мы также можем определить моделирование Монте-Карло в терминах Random[A]

def monteCarlo(test: Random[Boolean], trials: Int): Random[Double] = ...

и, наконец, функция вычисления PI

def pi(trials: Int): Random[Double] = ....

Все эти функции чисты. Побочные эффекты возникают только тогда, когда мы наконец применяем функцию pi для получения значения pi.

Ответы

Ответ 1

Ваш подход довольно приятный, хотя он немного сложный. Я также предложил вам взглянуть на главу 6 Функциональное программирование в Scala Пола Чиусано и Рунара Бьярнасона. Эта глава называется чисто функциональным состоянием, и она показывает, как создать чисто функциональный случайный генератор и определить его алгебру типа данных, а также полную поддержку композиции.