Czy konstruowanie obiektów stanowych powinno być modelowane za pomocą typu efektu?

9

Czy w przypadku korzystania z funkcjonalnego środowiska, takiego jak Scala cats-effect, czy budowę obiektów stanowych należy modelować za pomocą typu efektu?

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

Konstrukcja nie jest zawodna, więc moglibyśmy użyć słabszej klasy Apply.

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

Myślę, że wszystkie z nich są czyste i deterministyczne. Po prostu nie jest referencyjnie przejrzysty, ponieważ wynikowa instancja jest za każdym razem inna. Czy to dobry moment na użycie efektu typu? A może byłby tu inny wzór funkcjonalny?

Mark Canlas
źródło
2
Tak, tworzenie stanu zmiennego jest efektem ubocznym. W związku z tym powinno to nastąpić wewnątrz a delayi zwrócić F [usługę] . Jako przykład, patrz startmetoda na IO , zwraca IO [Fibre [IO,?]] , Zamiast zwykłego włókna.
Luis Miguel Mejía Suárez
1
Aby uzyskać pełną odpowiedź na ten problem, zobacz to i to .
Luis Miguel Mejía Suárez

Odpowiedzi:

3

Czy konstruowanie obiektów stanowych powinno być modelowane za pomocą typu efektu?

Jeśli już używasz systemu efektów, najprawdopodobniej ma on Reftyp bezpiecznie otaczający stan zmienny.

Mówię więc: modeluj obiekty stanowe za pomocąRef . Ponieważ tworzenie (a także dostęp do nich) jest już efektem, automatycznie sprawi, że tworzenie usługi również będzie skuteczne.

To starannie omija twoje pierwotne pytanie.

Jeśli chcesz ręcznie regularnie zarządzać wewnętrznym stanem mutable, varmusisz sam upewnić się, że wszystkie operacje, które dotykają tego stanu, są uważane za efekty (i najprawdopodobniej również są bezpieczne dla wątków), co jest uciążliwe i podatne na błędy. Można to zrobić i zgadzam się z odpowiedzią @ atl, że nie musisz ściśle tworzyć skutecznego obiektu państwowego (o ile możesz żyć z utratą integralności referencyjnej), ale dlaczego nie zaoszczędzić sobie kłopotów i objąć narzędzia twojego systemu efektów przez całą drogę?


Myślę, że wszystkie z nich są czyste i deterministyczne. Po prostu nie jest referencyjnie przejrzysty, ponieważ wynikowa instancja jest za każdym razem inna. Czy to dobry moment na użycie efektu typu?

Jeśli twoje pytanie można sformułować jako

Czy dodatkowe korzyści (oprócz poprawnie działającej implementacji przy użyciu „słabszej klasy”) przejrzystości referencyjnej i lokalnego rozumowania są wystarczające, aby uzasadnić użycie typu efektu (który musi być już używany do dostępu do stanu i mutacji) również do stanu kreacja ?

wtedy: tak, absolutnie .

Aby podać przykład, dlaczego jest to przydatne:

Poniższe działa dobrze, nawet jeśli tworzenie usługi nie przynosi efektu:

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

Ale jeśli zmienisz to tak, jak poniżej, nie pojawi się błąd czasu kompilacji, ale zmienisz zachowanie i najprawdopodobniej wprowadzisz błąd. Jeśli zadeklarujesz makeServiceskuteczność, refaktoryzacja nie sprawdzi typu i zostanie odrzucona przez kompilator.

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

Przyznanie nazwy metody jako makeService(i wraz z parametrem) powinno wyraźnie wyjaśnić, co robi metoda i że refaktoryzacja nie była bezpieczną rzeczą, ale „lokalne rozumowanie” oznacza, że ​​nie musisz szukać przy konwencjach nazewnictwa i implementacji, makeServiceaby wymyślić: Każde wyrażenie, którego nie można mechanicznie przetasować (deduplikowane, leniwe, chętne, eliminuje martwy kod, równoległe, opóźnione, buforowane, usuwane z pamięci podręcznej itp.) bez zmiany zachowania ( tzn. nie jest „czysty”) należy wpisać jako skuteczny.

Thilo
źródło
2

Czego dotyczy usługa stanowa w tym przypadku?

Czy masz na myśli, że wywoła efekt uboczny, gdy obiekt zostanie zbudowany? W tym celu lepszym pomysłem byłoby opracowanie metody uruchamiającej efekt uboczny podczas uruchamiania aplikacji. Zamiast uruchamiać go podczas budowy.

A może mówisz, że ma on zmienny stan w serwisie? Dopóki wewnętrzny stan zmienny nie jest odsłonięty, powinien być w porządku. Musisz tylko podać czystą (referencyjnie przejrzystą) metodę komunikacji z usługą.

Aby rozwinąć moją drugą uwagę:

Załóżmy, że budujemy bazę danych w pamięci.

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 nie musi to być skuteczne, ponieważ to samo dzieje się, gdy wykonujesz połączenie sieciowe. Musisz jednak upewnić się, że istnieje tylko jedna instancja tej klasy.

Jeśli używasz Refefektu koty, normalnie zrobiłbym to do flatMappunktu odniesienia w punkcie wejścia, więc twoja klasa nie musi być skuteczna.

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, jeśli piszesz usługę współdzieloną lub bibliotekę zależną od obiektu stanowego (powiedzmy wiele prymitywów współbieżności) i nie chcesz, aby użytkownicy dbali o to, co zainicjować.

Tak, to musi być owinięte w efekt. Możesz użyć czegoś takiego, Resource[F, MyStatefulService]aby upewnić się, że wszystko jest poprawnie zamknięte. Lub po prostu, F[MyStatefulService]jeśli nie ma nic do zamknięcia.

atl
źródło
„Wystarczy podać metodę czystą metodę komunikacji z usługą”, a może wręcz przeciwnie: początkowa konstrukcja stanu czysto wewnętrznego nie musi być efektem, ale każdą operacją usługi, która wchodzi w interakcję z tym stanem zmiennym w jakikolwiek sposób musi być oznaczony jako skuteczny (aby uniknąć wypadków takich jak val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
Lub pochodzący z drugiej strony: jeśli sprawisz, że tworzenie usługi będzie skuteczne, czy nie, nie jest to naprawdę ważne. Ale bez względu na to, w którą stronę pójdziesz, interakcja z tą usługą w jakikolwiek sposób musi być skuteczna (ponieważ przenosi w niej stan zmienny, na który wpływ będą miały te interakcje).
Thilo,
1
@thilo Tak, masz rację. Chodzi mi o pureto, że musi być referencyjnie przejrzysty. np. rozważ przykład z Future. val x = Future {... }i def x = Future { ... }oznacza inną rzecz. (To może cię ugryźć, gdy refaktoryzujesz kod) Ale nie jest tak w przypadku efektu kota, monixa lub zio.
atl