La costruzione di oggetti con stato dovrebbe essere modellata con un tipo di effetto?


9

Quando si utilizza un ambiente funzionale come Scala e cats-effect, la costruzione di oggetti con stato deve essere modellata con un tipo di effetto?

// 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 costruzione non è fallibile, quindi potremmo usare una classe di caratteri più debole come Apply.

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

Immagino che tutti questi siano puri e deterministici. Semplicemente non referenzialmente trasparente poiché l'istanza risultante è diversa ogni volta. È un buon momento per usare un tipo di effetto? O ci sarebbe un diverso modello funzionale qui?


2
Sì, la creazione di uno stato mutabile è un effetto collaterale. Come tale, dovrebbe accadere all'interno di a delaye restituire un F [Servizio] . Ad esempio, vedere il startmetodo su IO , restituisce un IO [Fibra [IO,?]] , Anziché la fibra semplice .
Luis Miguel Mejía Suárez,

1
Per una risposta completa a questo problema, vedere questo e questo .
Luis Miguel Mejía Suárez,

Risposte:


3

La costruzione di oggetti con stato dovrebbe essere modellata con un tipo di effetto?

Se stai già utilizzando un sistema di effetti, molto probabilmente ha un Reftipo per incapsulare in modo sicuro lo stato mutabile.

Quindi dico: modella oggetti con stato conRef . Poiché la creazione (oltre all'accesso a) di questi è già un effetto, questo renderà automaticamente efficace anche la creazione del servizio.

In questo modo, la tua domanda originale va di pari passo.

Se si desidera gestire manualmente uno stato mutabile interno con un regolare, varè necessario assicurarsi da soli che tutte le operazioni che toccano questo stato siano considerate effetti (e molto probabilmente anche rese thread-safe), che è noioso e soggetto a errori. Questo può essere fatto, e sono d'accordo con la risposta di @ atl che non devi rigorosamente rendere efficace la creazione dell'oggetto stateful (purché tu possa vivere con la perdita di integrità referenziale), ma perché non risparmiarti il ​​disturbo e abbracciarti gli strumenti del tuo sistema di effetti fino in fondo?


Immagino che tutti questi siano puri e deterministici. Semplicemente non referenzialmente trasparente poiché l'istanza risultante è diversa ogni volta. È un buon momento per usare un tipo di effetto?

Se la tua domanda può essere riformulata come

I vantaggi aggiuntivi (oltre a un'implementazione correttamente funzionante che utilizza una "classe di caratteri più debole") della trasparenza referenziale e del ragionamento locale sono sufficienti per giustificare l'uso di un tipo di effetto (che deve essere già in uso per l'accesso allo stato e la mutazione) anche per lo stato creazione?

allora: Sì, assolutamente .

Per fare un esempio del perché questo è utile:

Funziona bene anche se la creazione del servizio non ha effetto:

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

Ma se si esegue il refactoring come di seguito non si otterrà un errore in fase di compilazione, ma si sarà modificato il comportamento e molto probabilmente sarà stato introdotto un bug. Se fosse stato dichiarato makeServiceefficace, il refactoring non verificherebbe il controllo del tipo e sarebbe stato respinto dal compilatore.

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

Concedere la denominazione del metodo come makeService(e anche con un parametro) dovrebbe chiarire cosa fa il metodo e che il refactoring non era una cosa sicura da fare, ma "ragionamento locale" significa che non devi guardare alle convenzioni di denominazione e all'implementazione di makeServicecapirlo: qualsiasi espressione che non può essere mescolata meccanicamente (deduplicata, resa pigra, resa desiderosa, codice morto eliminato, parallelizzata, ritardata, memorizzata nella cache, eliminata da una cache ecc.) senza cambiare comportamento ( cioè non è "puro") dovrebbe essere digitato come efficace.


2

A cosa si riferisce il servizio con stato in questo caso?

Vuoi dire che eseguirà un effetto collaterale quando viene costruito un oggetto? Per questo, un'idea migliore sarebbe quella di avere un metodo che esegua l'effetto collaterale all'avvio dell'applicazione. Invece di eseguirlo durante la costruzione.

O forse stai dicendo che contiene uno stato mutevole all'interno del servizio? Finché lo stato mutabile interno non è esposto, dovrebbe essere a posto. Devi solo fornire un metodo puro (referenzialmente trasparente) per comunicare con il servizio.

Per espandere il mio secondo punto:

Diciamo che stiamo costruendo un db in memoria.

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, questo non deve essere efficace, poiché la stessa cosa accade se si effettua una chiamata di rete. Tuttavia, è necessario assicurarsi che esista solo un'istanza di questa classe.

Se stai usando l' Refeffetto gatti, ciò che farei normalmente è flatMapall'arbitro al punto di ingresso, quindi la tua classe non deve essere 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, se stai scrivendo un servizio condiviso o una libreria che dipende da un oggetto stateful (diciamo più primitive sulla concorrenza) e non vuoi che i tuoi utenti si preoccupino di cosa inizializzare.

Quindi sì, deve essere racchiuso in un effetto. Puoi usare qualcosa del genere, Resource[F, MyStatefulService]per assicurarti che tutto sia chiuso correttamente. O solo F[MyStatefulService]se non c'è niente da chiudere.


"Devi solo fornire un metodo un metodo puro per comunicare con il servizio" O forse esattamente il contrario: la costruzione iniziale di uno stato puramente interno non deve necessariamente avere un effetto, ma qualsiasi operazione sul servizio che interagisce con quello stato mutabile in qualsiasi modo quindi deve essere contrassegnato come efficace (per evitare incidenti come val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo

O proveniente dall'altra parte: se rendi efficace la creazione del servizio o meno, non è molto importante. Ma non importa in quale direzione tu vada, l'interazione con quel servizio in ogni modo deve essere efficace (perché porta all'interno uno stato mutevole che sarà influenzato da queste interazioni).
Thilo,

1
@thilo Sì, hai ragione. Quello che intendevo dire pureè che deve essere referenzialmente trasparente. ad esempio prendere in considerazione un esempio con Future. val x = Future {... }e def x = Future { ... }significa una cosa diversa. (Questo può morderti quando stai eseguendo il refactoring del tuo codice) Ma non è il caso dell'effetto gatti, del monix o dello zio.
atl
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.