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?
scala
functional-programming
scala-cats
cats-effect
Mark Canlas
źródło
źródło
delay
i zwrócić F [usługę] . Jako przykład, patrzstart
metoda na IO , zwraca IO [Fibre [IO,?]] , Zamiast zwykłego włókna.Odpowiedzi:
Jeśli już używasz systemu efektów, najprawdopodobniej ma on
Ref
typ 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,
var
musisz 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ę?Jeśli twoje pytanie można sformułować jako
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:
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
makeService
skuteczność, refaktoryzacja nie sprawdzi typu i zostanie odrzucona przez kompilator.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,makeService
aby 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.źródło
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.
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
Ref
efektu koty, normalnie zrobiłbym to doflatMap
punktu odniesienia w punkcie wejścia, więc twoja klasa nie musi być skuteczna.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.źródło
val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
)pure
to, że musi być referencyjnie przejrzysty. np. rozważ przykład z Future.val x = Future {... }
idef x = Future { ... }
oznacza inną rzecz. (To może cię ugryźć, gdy refaktoryzujesz kod) Ale nie jest tak w przypadku efektu kota, monixa lub zio.