Чи слід побудувати об’єкти стану, що моделюються, з типом ефекту?


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 [послугу] . Як приклад, див. startМетод на IO , він повертає IO [Fiber [IO,?]] Замість простого волокна.
Луїс Мігель Мехіа Суарес

1
Щоб отримати повну відповідь на цю проблему, будь ласка, дивіться це & це .
Луїс Мігель Мехіа Суарес

Відповіді:


3

Чи слід побудувати об’єкти стану, що моделюються, з типом ефекту?

Якщо ви вже використовуєте систему ефектів, вона, швидше за все, має a 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

На що посилається державна служба в цьому випадку?

Ви маєте на увазі, що він буде виконувати побічний ефект, коли об’єкт будується? Для цього кращою ідеєю було б мати метод, який запускає побічний ефект при запуску програми. Замість того, щоб запускати його під час будівництва.

Чи, можливо, ви говорите, що він зберігає стан, що змінюється, всередині служби? Поки внутрішній стан, що змінюється, не піддається впливу, він повинен бути добре. Вам просто потрібно надати чистий (референтно прозорий) метод спілкування зі службою.

Щоб розширити свою другу точку:

Скажімо, ми будуємо db пам'яті.

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

Або з іншого боку: Якщо ви робите створення послуги ефективним чи ні, це не дуже важливо. Але незалежно від того, яким шляхом ви йдете, будь-яка взаємодія з цією службою повинна бути ефективною (оскільки вона всередині себе змінює стан, що впливатиме на ці взаємодії).
Тіло

1
@thilo Так, ти маєш рацію. Я мав на увазі те, pureщо він повинен бути референтно прозорим. наприклад, розглянемо приклад з майбутнім. val x = Future {... }і def x = Future { ... }означає іншу річ. (Це може вкусити вас, коли ви рефакторинг свого коду) Але це не так з ефектом котів, монікс або zio.
Атль
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.