Ответ 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])