Должно ли моделирование объектов с состоянием моделироваться с типом эффекта?

9

При использовании функциональной среды, такой как Scala cats-effect, следует ли моделировать объекты с состоянием с типом эффекта?

// not a value/case class
class Service(s: name)

def withoutEffect(name: String): Service =
  new Service(name)

def withEffect[F: Sync](name: String): F[Service] =
  F.delay {
    new Service(name)
  }

Конструкция не подвержена ошибкам, поэтому мы могли бы использовать более слабый класс типов, например Apply.

// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
  new Service(name).pure[F]

Я думаю, все это чисто и детерминистично. Просто не ссылочно прозрачно, так как результирующий экземпляр каждый раз отличается. Это хорошее время для использования типа эффекта? Или здесь будет другой функциональный паттерн?

Марк Канлас
источник
2
Да, создание изменяемого состояния является побочным эффектом. Таким образом, это должно происходить внутри a delayи возвращать F [Service] . В качестве примера, см. startМетод ввода-вывода , он возвращает IO [Fiber [IO,?]] Вместо простого волокна.
Луис Мигель Мехия Суарес
1
Для полного ответа на эту проблему, пожалуйста, посмотрите это и это .
Луис Мигель Мехия Суарес

Ответы:

3

Должно ли моделирование объектов с состоянием моделироваться с типом эффекта?

Если вы уже используете систему эффектов, она, скорее всего, имеет Refтип для безопасной инкапсуляции изменяемого состояния.

Поэтому я говорю: моделировать объекты с состоянием сRef . Поскольку создание (а также доступ к ним) уже является результатом, это автоматически сделает создание службы эффективным.

Это аккуратно обходит ваш первоначальный вопрос.

Если вы хотите вручную управлять внутренним изменяемым состоянием с помощью обычного, varвы должны сами убедиться, что все операции, которые касаются этого состояния, считаются эффектами (и, скорее всего, также сделаны поточно-ориентированными), что утомительно и подвержено ошибкам. Это можно сделать, и я согласен с ответом @ atl, что вам не обязательно делать создание объекта с сохранением состояния эффективным (если вы можете жить с потерей ссылочной целостности), но почему бы не избавить себя от проблем и не принять инструменты вашей системы эффектов полностью?


Я думаю, все это чисто и детерминистично. Просто не ссылочно прозрачно, так как результирующий экземпляр каждый раз отличается. Это хорошее время для использования типа эффекта?

Если ваш вопрос можно перефразировать как

Достаточно ли дополнительных преимуществ (помимо правильно работающей реализации, использующей «более слабый класс типов») в отношении ссылочной прозрачности и локальных рассуждений, чтобы оправдать использование типа эффекта (который уже используется для доступа к состоянию и его мутации) также для состояния создание ?

тогда: да, абсолютно .

Чтобы дать пример того, почему это полезно:

Следующее работает нормально, хотя создание сервиса не дает эффекта:

val service = makeService(name)
for {
  _ <- service.doX()
  _ <- service.doY()
} yield Ack.Done

Но если вы выполните рефакторинг, как показано ниже, вы не получите ошибку во время компиляции, но вы изменили поведение и, скорее всего, ввели ошибку. Если бы вы объявили makeServiceэффективный, рефакторинг не будет проверять тип и будет отклонен компилятором.

for {
  _ <- makeService(name).doX()
  _ <- makeService(name).doY()
} yield Ack.Done

Если присваивать имя методу как makeService(и с параметром тоже), должно быть достаточно ясно, что метод делает, и что рефакторинг не безопасен, но «локальные рассуждения» означают, что вам не нужно искать в соглашениях об именах и реализации, makeServiceчтобы понять это: любое выражение, которое не может быть механически перемешано (дедуплицировано, сделано ленивым, сделано нетерпеливым, мертвый код удалено, распараллелено, отложено, кэшировано, удалено из кэша и т. д.) без изменения поведения ( т.е. не является "чистым") следует набирать как эффективный.

Тило
источник
2

Что означает служба с сохранением состояния в этом случае?

Вы имеете в виду, что он будет выполнять побочный эффект при создании объекта? Для этого лучшей идеей было бы иметь метод, который запускает побочный эффект при запуске приложения. Вместо того, чтобы запустить его во время строительства.

Или, может быть, вы говорите, что он содержит изменяемое состояние внутри службы? Пока внутреннее изменчивое состояние не выставлено, оно должно быть в порядке. Вам просто нужно предоставить чистый (ссылочно прозрачный) метод для связи со службой.

Чтобы расширить мой второй пункт:

Допустим, мы создаем в памяти БД.

class InMemoryDB(private val hashMap: ConcurrentHashMap[String, String]) {
  def getId(s: String): IO[String] = ???
  def setId(s: String): IO[Unit] = ???
}

object InMemoryDB {
  def apply(hashMap: ConcurrentHashMap[String, String]) = new InMemoryDB(hashMap)
}

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

Если вы используете Refэффект от кошек, то, что я обычно делаю, это flatMapссылка на точку входа, поэтому ваш класс не должен быть эффективным.

object Effectful extends IOApp {

  class InMemoryDB(storage: Ref[IO, Map[String, String]]) {
    def getId(s: String): IO[String] = ???
    def setId(s: String): IO[Unit] = ???
  }

  override def run(args: List[String]): IO[ExitCode] = {
    for {
      storage <- Ref.of[IO, Map[String, String]](Map.empty[String, String])
      _ = app(storage)
    } yield ExitCode.Success
  }

  def app(storage: Ref[IO, Map[String, String]]): InMemoryDB = {
    new InMemoryDB(storage)
  }
}

OTOH, если вы пишете общую службу или библиотеку, которая зависит от объекта с состоянием (скажем, от нескольких примитивов параллелизма), и вы не хотите, чтобы ваши пользователи заботились о том, что инициализировать.

Тогда, да, это должно быть завернуто в эффект. Вы можете использовать что-то вроде, Resource[F, MyStatefulService]чтобы убедиться, что все закрыто правильно. Или просто F[MyStatefulService]если нечего закрывать.

атль
источник
«Вам просто нужно предоставить методу чистый метод для связи со службой». Или, может быть, как раз наоборот: первоначальное построение чисто внутреннего состояния не должно быть следствием, но любая операция над службой, которая взаимодействует с этим изменяемым состоянием в в любом случае, тогда нужно пометить эффектно (чтобы избежать подобных аварий val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
Или с другой стороны: если вы делаете это создание сервиса эффективным или нет, это не очень важно. Но независимо от того, каким путем вы идете, взаимодействие с этим сервисом любым способом должно быть эффективным (потому что оно несет изменяемое состояние внутри, на которое будут влиять эти взаимодействия).
Thilo
1
@thilo Да, ты прав. Я имел в виду, pureчто он должен быть прозрачным по ссылкам. Например, рассмотрим пример с Future. val x = Future {... }и def x = Future { ... }означает другое. (Это может укусить вас, когда вы делаете рефакторинг своего кода) Но это не относится к эффектам кошки, monix или zio.
атль