Состояние Scala, чистота функций и разделение ответственности

Я не могу справиться с тем, как состояние должно быть реализовано в приложении 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 класс. Заставляет знать и помнить о вещах, которые должны быть прозрачными, не так ли?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете просто использовать встроенный 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)      
    }
}

Вы можете увидеть работающий код здесь.

Я считаю, что это отличное решение, но в этом случае я хотел бы сохранить свою форму, поэтому не применяйте, mapN, но класс и «внешний вид» метода generate. Я упростил пример, потому что в реальном случае происходит гораздо больше. Интересно, можно ли изменить состояние LastValue методом generate.

LancerX 10.04.2023 00:17

@LancerX Извините, но я не понимаю, что вы имеете в виду под «нет apply, mapN», а также WDYM с _ «видом метода generate»? - Вы можете просто переименовать next из моего примера в generate и можете просто использовать класс вместо анонимного экземпляра, как это сделал я. -Суть в том, что вы управляете состоянием с помощью Ref, а создание общего состояния должно возвращать IO.

Luis Miguel Mejía Suárez 10.04.2023 01:21

Только так я понимаю использование Ref в моем примере pastebin.com/5Tv8a6rc Но это ничего не меняет, поэтому я делаю это неправильно

LancerX 10.04.2023 13:35

@LancerX да, потому что вам нужно перенести создание общего состояния (то есть Ref) за пределы Generator. Другими словами, создание Generator должно возвращать IO[Generator] вместо простого Generator, потому что оно содержит состояние. - Таким образом, то, что вы можете сделать, это class Generator(state: Ref[IO, Int]), и обычно мы скрываем это, делая конструктор закрытым и имея фабрику в сопутствующем объекте, так что мы гарантируем, что Ref не является общим (что само по себе не является неправильным, но обычно не то, что вы хотите), но вы просто оставляете все как есть и позволяете звонящему передать Ref.

Luis Miguel Mejía Suárez 10.04.2023 15:18

Спасибо, попробую победить зверя :)

LancerX 10.04.2023 16:27

@LancerX кстати, последний «вариант», если вы не можете создать конструкцию Generator для возврата IO, заключается в unsafeRunSync создании Ref в теле класса, таким образом, вы получите изменяемое состояние из IO и вы можете использовать его, как вы пытались. Обратите внимание, однако, что это технически «неправильно».

Luis Miguel Mejía Suárez 10.04.2023 17:24

Другие вопросы по теме