Я не могу справиться с тем, как состояние должно быть реализовано в приложении Scala. Предположим, я хочу иметь генератор чисел, но сгенерированные числа не являются полностью случайными. Они зависят от предыдущего сгенерированного числа. Итак, пусть у нас есть начальное 33, и в каждом следующем вызове я хочу увеличить 33 на число от 0 до 10, например. 33, 35, 41, 42, 49... Пробовал так:
class Generator {
private val state: IO[LastValue] = IO.pure(33).flatMap(i => lastValue(i))
trait LastValue {
def change(delta: Int): IO[Unit]
def get: IO[Int]
}
private def lastValue(initial: Int): IO[LastValue] = IO.delay {
var lastValue = initial
new LastValue {
override def change(newVal: Int): IO[Unit] = IO.delay { lastValue = newVal }
override def get: IO[Int] = IO.delay { lastValue }
}
}
def generate: IO[Int] =
for {
delta <- IO.delay { scala.util.Random.nextInt(10) }
lastVal <- state.flatMap(_.get)
newVal = lastVal + delta
_ <- state.flatMap(state => state.change(newVal))
} yield newVal
}
но, конечно, _ <- state.flatMap(state => state.change(newVal))
не изменяет мой val state
. Есть ли способ сделать это правильно?
Я предполагаю, что этот дизайн может не соответствовать принципу чистоты функций. И я согласен, но с другой стороны, это право заставлять пользователя Generator
(назовем этот класс User
) заботиться о состоянии Генератора? Я имею в виду изменить его на def generate(prevGenerator: Generator): IO[(Generator, Int)]
, а generator
можно сохранить в User
, чтобы передать его в следующем вызове.
Но это загрязняет User
класс. Заставляет знать и помнить о вещах, которые должны быть прозрачными, не так ли?
Вы можете просто использовать встроенный Ref
, который предназначен для управления параллельным изменяемым состоянием в режиме реального времени.
Вы также должны предпочесть встроенный Random
.
trait Generator {
def next: IO[Int]
}
object Generator {
def apply(init: Int, step: Int, seed: Int): IO[Generator] =
(
IO.ref(init),
Random.scalaUtilRandomSeedInt[IO](seed)
).mapN {
case (ref, rnd) =>
new Generator {
override final val next: IO[Int] =
rnd.nextIntBounded(step + 1).flatMap { inc =>
ref.updateAndGet(i => i + inc)
}
}
}
}
Обратите внимание, как мы скрываем обе детали реализации от пользователя.
Который затем можно использовать следующим образом:
object Main extends IOApp.Simple {
override final val run: IO[Unit] =
Generator(init = 33, step = 10, seed = 135).flatMap { gen =>
gen.next.flatMap(IO.println).replicateA_(10)
}
}
Вы можете увидеть работающий код здесь.
@LancerX Извините, но я не понимаю, что вы имеете в виду под «нет apply
, mapN
», а также WDYM с _ «видом метода generate
»? - Вы можете просто переименовать next
из моего примера в generate
и можете просто использовать класс вместо анонимного экземпляра, как это сделал я. -Суть в том, что вы управляете состоянием с помощью Ref
, а создание общего состояния должно возвращать IO
.
Только так я понимаю использование Ref
в моем примере pastebin.com/5Tv8a6rc Но это ничего не меняет, поэтому я делаю это неправильно
@LancerX да, потому что вам нужно перенести создание общего состояния (то есть Ref
) за пределы Generator
. Другими словами, создание Generator
должно возвращать IO[Generator]
вместо простого Generator
, потому что оно содержит состояние. - Таким образом, то, что вы можете сделать, это class Generator(state: Ref[IO, Int])
, и обычно мы скрываем это, делая конструктор закрытым и имея фабрику в сопутствующем объекте, так что мы гарантируем, что Ref
не является общим (что само по себе не является неправильным, но обычно не то, что вы хотите), но вы просто оставляете все как есть и позволяете звонящему передать Ref
.
Спасибо, попробую победить зверя :)
@LancerX кстати, последний «вариант», если вы не можете создать конструкцию Generator
для возврата IO
, заключается в unsafeRunSync
создании Ref
в теле класса, таким образом, вы получите изменяемое состояние из IO
и вы можете использовать его, как вы пытались. Обратите внимание, однако, что это технически «неправильно».
Я считаю, что это отличное решение, но в этом случае я хотел бы сохранить свою форму, поэтому не применяйте, mapN, но класс и «внешний вид» метода
generate
. Я упростил пример, потому что в реальном случае происходит гораздо больше. Интересно, можно ли изменить состояниеLastValue
методомgenerate
.