Lorsque vous utilisez un environnement fonctionnel comme Scala et cats-effect
, la construction d'objets avec état doit-elle être modélisée avec un type d'effet?
// 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)
}
La construction n'est pas faillible, nous pourrions donc utiliser une classe de type plus faible comme Apply
.
// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
new Service(name).pure[F]
Je suppose que tout cela est pur et déterministe. Juste pas référentiellement transparent car l'instance résultante est différente à chaque fois. Est-ce le bon moment pour utiliser un type d'effet? Ou y aurait-il un modèle fonctionnel différent ici?
scala
functional-programming
scala-cats
cats-effect
Mark Canlas
la source
la source
delay
et renvoyer un F [Service] . A titre d'exemple, voir lastart
méthode sur IO , elle renvoie une IO [Fibre [IO,?]] , Au lieu de la fibre ordinaire .Réponses:
Si vous utilisez déjà un système d'effets, il a très probablement un
Ref
type pour encapsuler en toute sécurité un état mutable.Je dis donc: modélisez les objets avec état avec
Ref
. Étant donné que la création (ainsi que l'accès à) ceux-ci est déjà un effet, cela rendra automatiquement la création du service également efficace.Cela contourne parfaitement votre question d'origine.
Si vous voulez gérer manuellement l'état mutable interne avec un habitué,
var
vous devez vous assurer que toutes les opérations qui touchent cet état sont considérées comme des effets (et très probablement également rendues thread-safe), ce qui est fastidieux et sujet aux erreurs. Cela peut être fait, et je suis d'accord avec la réponse de @ atl selon laquelle vous n'avez pas strictement à rendre la création de l'objet avec état efficace (tant que vous pouvez vivre avec la perte de l'intégrité référentielle), mais pourquoi ne pas vous épargner la peine et l'embrasser les outils de votre système d'effets jusqu'au bout?Si votre question peut être reformulée comme
alors: oui, absolument .
Pour donner un exemple de la raison pour laquelle cela est utile:
Les éléments suivants fonctionnent bien, même si la création de services n'est pas effectuée:
Mais si vous refactorisez ceci comme ci-dessous, vous n'obtiendrez pas d'erreur de compilation, mais vous aurez changé le comportement et vous aurez probablement introduit un bogue. Si vous aviez déclaré
makeService
efficace, la refactorisation ne serait pas vérifiée par type et serait rejetée par le compilateur.Accorder le nom de la méthode comme
makeService
(et avec un paramètre aussi) devrait rendre assez clair ce que fait la méthode et que le refactoring n'était pas une chose sûre à faire, mais le "raisonnement local" signifie que vous n'avez pas à regarder lors des conventions de dénomination et de la mise en œuvre demakeService
pour comprendre cela: toute expression qui ne peut pas être mélangée mécaniquement (dédupliquée, rendue paresseuse, rendue impatiente, code mort éliminé, parallélisée, retardée, mise en cache, purgée d'un cache, etc.) sans changer de comportement ( c'est-à-dire qu'il n'est pas "pur") doit être saisi comme efficace.la source
À quoi fait référence le service avec état dans ce cas?
Voulez-vous dire qu'il exécutera un effet secondaire lorsqu'un objet est construit? Pour cela, une meilleure idée serait d'avoir une méthode qui exécute l'effet secondaire au démarrage de votre application. Au lieu de l'exécuter pendant la construction.
Ou peut-être que vous dites qu'il détient un état mutable à l'intérieur du service? Tant que l'état mutable interne n'est pas exposé, il devrait être correct. Il vous suffit de fournir une méthode pure (référentiellement transparente) pour communiquer avec le service.
Pour développer mon deuxième point:
Disons que nous construisons une base de données en mémoire.
OMI, cela n'a pas besoin d'être efficace, car la même chose se produit si vous effectuez un appel réseau. Cependant, vous devez vous assurer qu'il n'y a qu'une seule instance de cette classe.
Si vous utilisez l'
Ref
effet chats, ce que je ferais normalement, c'estflatMap
la référence au point d'entrée, donc votre classe n'a pas besoin d'être efficace.OTOH, si vous écrivez un service partagé ou une bibliothèque qui dépend d'un objet avec état (disons plusieurs primitives de concurrence) et que vous ne voulez pas que vos utilisateurs se soucient de quoi initialiser.
Ensuite, oui, il doit être enveloppé dans un effet. Vous pouvez utiliser quelque chose comme,
Resource[F, MyStatefulService]
pour vous assurer que tout est bien fermé. Ou justeF[MyStatefulService]
s'il n'y a rien à fermer.la source
val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
)pure
là, c'est qu'il doit être transparent par référence. Par exemple, considérons un exemple avec Future.val x = Future {... }
etdef x = Future { ... }
signifie une chose différente. (Cela peut vous mordre lorsque vous refactorisez votre code) Mais ce n'est pas le cas avec les effets de chats, monix ou zio.