Понимание случайной монады в 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 Пола Чиусано и Рунара Бьярнасона.
Эта глава называется чисто функциональным состоянием, и она показывает, как создать чисто функциональный случайный генератор и определить его алгебру типа данных, а также полную поддержку композиции.