La construction d'objets avec état doit-elle être modélisée avec un type d'effet?

9

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?

Mark Canlas
la source
2
Oui, la création d'un état mutable est un effet secondaire. En tant que tel, cela doit se produire à l'intérieur de a delayet renvoyer un F [Service] . A titre d'exemple, voir la startméthode sur IO , elle renvoie une IO [Fibre [IO,?]] , Au lieu de la fibre ordinaire .
Luis Miguel Mejía Suárez
1
Pour une réponse complète à ce problème, veuillez consulter ceci et ceci .
Luis Miguel Mejía Suárez

Réponses:

3

La construction d'objets avec état doit-elle être modélisée avec un type d'effet?

Si vous utilisez déjà un système d'effets, il a très probablement un Reftype pour encapsuler en toute sécurité un état mutable.

Je dis donc: modélisez les objets avec état avecRef . É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é, varvous 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?


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?

Si votre question peut être reformulée comme

Les avantages supplémentaires (en plus d'une implémentation fonctionnant correctement en utilisant une "classe de types plus faible") de transparence référentielle et de raisonnement local suffisent-ils à justifier l'utilisation d'un type d'effet (qui doit déjà être utilisé pour l'accès et la mutation de l'État) également pour l'État création ?

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:

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

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é makeServiceefficace, la refactorisation ne serait pas vérifiée par type et serait rejetée par le compilateur.

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

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 de makeServicepour 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.

Thilo
la source
2

À 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.

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

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' Refeffet chats, ce que je ferais normalement, c'est flatMapla référence au point d'entrée, donc votre classe n'a pas besoin d'être efficace.

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, 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 juste F[MyStatefulService]s'il n'y a rien à fermer.

atl
la source
"Il vous suffit de fournir une méthode, une méthode pure pour communiquer avec le service" Ou peut-être tout le contraire: la construction initiale d'un état purement interne n'a pas besoin d'être un effet, mais toute opération sur le service qui interagit avec cet état tout moyen doit alors être marqué comme efficace (pour éviter les accidents comme val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
Ou venant de l'autre côté: si vous rendez cette création de service efficace ou non, ce n'est pas vraiment important. Mais quelle que soit la façon dont vous allez, l'interaction avec ce service doit être efficace (car il porte un état mutable à l'intérieur qui sera affecté par ces interactions).
Thilo
1
@thilo Oui, vous avez raison. Ce que j'entendais par purelà, c'est qu'il doit être transparent par référence. Par exemple, considérons un exemple avec Future. val x = Future {... }et def 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.
atl