Search code examples
scalastatescala-catscats-effect

Scala state, function purity and separation of responsibility


I can't cope with how state should be implemented in Scala app. Assume I want to have a number generator but generated numbers are not totally random. They depend on the previous generated number. So let's have initial 33 and in each next call I want to increase 33 by number from 0 to 10 e.g. 33, 35, 41, 42, 49... I tried this way:

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
}

but of course _ <- state.flatMap(state => state.change(newVal)) does not modify my val state. Is any way to do it right that?

I guess this design may not fulfill the function purity principle. And I agree but on the other hand, is this right to force user of the Generator (let's call this class User) to care about the Generator's state? I mean to modify it into def generate(prevGenerator: Generator): IO[(Generator, Int)] and generator can be stored in User to pass is in the next call. But it pollutes User class. Forces to know and remember about things which should be transparent, doesn't it?


Solution

  • You can just use the built-in Ref which is designed to manage concurrent mutable state in a RT way.
    You also should prefer the built-in 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)
                }
            }
        }
    }
    

    Note how we hide both implementation details from the user.

    Which then can be used like this:

    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)      
        }
    }
    

    You can see the code running here.