Как создать случайное число с использованием функционального состояния?

Я пытаюсь выяснить, как объединить функциональное представление состояния с классом Scala Random для генерации случайных целых чисел. Я изучаю книгу "Функциональное программирование" в Scala, и поэтому большая часть кода берется оттуда.

Вот что выглядит класс State, непосредственно из книги:

case class State[S, +A](run: S => (A, S))

И вот что я хочу сделать:

object State {
  type Rand[A] = State[A, Random] // the Random from scala.util.Random

  def nextIntInRange(from: Int, to: Int): Rand[Int] =
  ??? _.nextInt(from - to) + from ??? // unsure about much of this

  def get(a: Rand[A]): A = ??? // also unsure; should modify state

  def getAndPreserveState(a: Rand[A]): A = ??? // ditto; should not modify state

  def diceRolls(n: Int) = {
    val roll = nextIntInRange(1, 6) 
    go(n: Int, acc: List[Int]): List[Int] = {
      if (n >= 0) go(n-1, get(roll) :: acc) else acc
    }
    go(n, List())
  }

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

Моя цель - использовать diceRolls с любым размером целого и для любого заданного начального семестра и генерировать список целых чисел, которые никогда не изменяются. Другими словами, diceRolls(3) может быть List(3,3,2), и если это так, переписывание его как diceRolls(7).take(3) должно снова появиться в List(3,3,2) и т.д.

Ответ 1

Мы хотим генерировать случайные числа и сохранять генератор случайных чисел (на данный момент RNG) (типа scala.util.Random) как состояние в вашем классе State.

Мы можем определить тип Rand[A] как:

type Rand[A] = State[Random, A]

Мы хотим иметь возможность получить случайное целое число в диапазоне. Если у нас есть RNG, это можно легко сделать, используя:

def randomInRange(rng: Random, start: Int, end: Int) = 
  rng.nextInt(end - start + 1) + start

randomInRange(new Random(1L), 10, 20) // Int = 14

Но мы хотим использовать RNG из предыдущего состояния, поэтому мы определяем a State с тем же кодом в функции run:

def nextIntInRange(from: Int, to: Int): Rand[Int] = 
  State((r: Random) => (r.nextInt(to - from + 1) + from, r))

Наша функция nextIntInRange возвращает случайное число и RNG. Позволяет определить roll для его проверки:

val roll = nextIntInRange(1, 6)

val rng = new Random(1L)
val (one, rng2) = roll.run(rng)
// (Int, scala.util.Random) = (4,[email protected])
val (two, rng3) = roll.run(rng2)
// (Int, scala.util.Random) = (5,[email protected])

До сих пор так хорошо, что мы могли бы подумать, но если мы будем использовать rng два раза, мы хотели бы получить одно и то же случайное число:

val rng = new Random(1L)
val (one, _) = roll.run(rng) // 4
val (two, _) = roll.run(rng) // 5

У нас есть два разных числа, это не то, что мы хотим, когда используем State. Мы хотели бы, чтобы рулон с использованием того же RNG возвращал тот же результат. Проблема в том, что Random мутирует его внутреннее состояние, поэтому мы не можем поместить последующие изменения состояния в State.

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

Хотя использование Random поражает цель использования State, мы можем попытаться реализовать остальные функции в качестве учебного упражнения.

Давайте посмотрим на get и getAndPreserveState:

def get(a: Rand[A]): A = ??? // also unsure; should modify state

def getAndPreserveState(a: Rand[A]): A = ??? // ditto; should not modify state

Если мы посмотрим на сигнатуры типа, нам нужно передать Rand[A], как наша функция roll, и вернуть результат этой функции. Эти функции странны из-за нескольких причин:

  • Наша функция roll нуждается в экземпляре Random для получения результата, но у нас нет параметра типа Random.
  • Тип возврата A, поэтому если бы мы имели экземпляр Random, мы могли бы вернуть только случайное число после вызова a.run(ourRng), но что мы должны делать с нашим состоянием. Мы хотим сохранить наше состояние явно.

Позволяет оставить эту функцию, которая пытается потерять наше драгоценное состояние и реализовать финальную функцию diceRolls, в которой мы хотим пару раз кусочек и вернуть список случайных чисел. Таким образом, тип этой функции будет Rand[List[Int]].

У нас уже есть функция roll, теперь нам нужно использовать ее несколько раз, что мы могли бы сделать с помощью List.fill:

List.fill(10)(roll) // List[Rand[Int]] 

Однако результирующий тип List[Rand[Int]], а не Rand[List[Int]]. Преобразование из F[G[_]] в G[F[_]] - это общая операция, называемая sequence, позволяет реализовать ее непосредственно для State:

object State {

  def sequence[A, S](xs: List[State[S, A]]): State[S, List[A]] = {
    def go[S, A](list: List[State[S, A]], accState: State[S, List[A]]): State[S, List[A]] = 
      list match { 
        // we have combined all States, lets reverse the accumulated list
        case Nil => 
          State((inputState: S) => {
            val (accList, state) = accState.run(inputState)
            (accList.reverse, state)
          })
        case stateTransf :: tail => 
          go(
            tail,
            State((inputState: S) => {
              // map2
              val (accList, oldState) = accState.run(inputState) 
              val (a, nextState) = stateTransf.run(oldState)
              (a :: accList, nextState) 
            })
          )
      }
    // unit
    go(xs, State((s: S) => (List.empty[A], s)))
  }

}

Некоторое объяснение нашего случая с Rand[Int]:

// use the RNG in to create the previous random numbers
val (accList, oldState) = accState.run(inputState)
// generate a new random number 
val (a, nextState) = stateTransf.run(oldState)
// add the randomly generated number to the already generated random numbers
// and return the new state of the RNG
(a :: accList, nextState) 

Моя реализация State.sequence может быть значительно очищена путем определения функции unit и map2, как это было сделано в ответы fpinscala на GitHub.

Теперь мы можем определить нашу функцию diceRolls как:

def diceRolls(n: Int) = State.sequence(List.fill(n)(roll))

Что мы можем использовать как:

diceRolls(5).run(new Random(1L))
// (List[Int], scala.util.Random) = (List(4, 5, 2, 4, 3),[email protected])