Sollte das Erstellen zustandsbehafteter Objekte mit einem Effekttyp modelliert werden?

9

Sollte bei Verwendung einer funktionalen Umgebung wie Scala und cats-effectdie Konstruktion zustandsbehafteter Objekte mit einem Effekttyp modelliert werden?

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

Die Konstruktion ist nicht fehlbar, daher könnten wir eine schwächere Typklasse wie verwenden Apply.

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

Ich denke, all dies ist rein und deterministisch. Nur nicht referenziell transparent, da die resultierende Instanz jedes Mal anders ist. Ist das ein guter Zeitpunkt, um einen Effekttyp zu verwenden? Oder würde es hier ein anderes Funktionsmuster geben?

Mark Canlas
quelle
2
Ja, die Schaffung eines veränderlichen Zustands ist ein Nebeneffekt. Als solches sollte es innerhalb von a geschehen delayund einen F [Service] zurückgeben . Beispiel: Die startMethode für E / A gibt anstelle der einfachen Faser eine E / A [Fiber [IO ,?]] Zurück.
Luis Miguel Mejía Suárez
1
Eine vollständige Antwort auf dieses Problem finden Sie hier und hier .
Luis Miguel Mejía Suárez

Antworten:

3

Sollte das Erstellen zustandsbehafteter Objekte mit einem Effekttyp modelliert werden?

Wenn Sie bereits ein Effektsystem verwenden, hat es höchstwahrscheinlich einen RefTyp, der den veränderlichen Zustand sicher einkapselt.

Also sage ich: modelliere Stateful Objects mitRef . Da das Erstellen (sowie der Zugriff darauf) bereits ein Effekt ist, wird das Erstellen des Dienstes automatisch ebenfalls effektiv.

Dies umgeht Ihre ursprüngliche Frage ordentlich.

Wenn Sie den internen veränderlichen Status manuell mit einem regulären Status verwalten möchten, müssen varSie selbst sicherstellen, dass alle Vorgänge, die diesen Status berühren, als Auswirkungen betrachtet werden (und höchstwahrscheinlich auch threadsicher gemacht werden), was langwierig und fehleranfällig ist. Dies kann getan werden, und ich stimme der Antwort von @ atl zu, dass Sie die Erstellung des zustandsbehafteten Objekts nicht unbedingt effektiv machen müssen (solange Sie mit dem Verlust der referenziellen Integrität leben können), aber warum sparen Sie sich nicht die Mühe und die Umarmung die Werkzeuge Ihres Effektsystems den ganzen Weg?


Ich denke, all dies ist rein und deterministisch. Nur nicht referenziell transparent, da die resultierende Instanz jedes Mal anders ist. Ist das ein guter Zeitpunkt, um einen Effekttyp zu verwenden?

Wenn Ihre Frage umformuliert werden kann als

Sind die zusätzlichen Vorteile (zusätzlich zu einer korrekt funktionierenden Implementierung mit einer "schwächeren Typklasse") der referenziellen Transparenz und der lokalen Argumentation ausreichend, um die Verwendung eines Effekttyps (der bereits für den Zugriff und die Mutation des Staates verwendet werden muss) auch für den Staat zu rechtfertigen? Schaffung ?

dann: Ja, absolut .

Um ein Beispiel zu geben, warum dies nützlich ist:

Folgendes funktioniert einwandfrei, obwohl die Erstellung von Diensten keine Auswirkungen hat:

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

Wenn Sie dies jedoch wie folgt umgestalten, wird beim Kompilieren kein Fehler angezeigt, aber Sie haben das Verhalten geändert und höchstwahrscheinlich einen Fehler eingeführt. Wenn Sie für makeServicewirksam erklärt hätten, würde das Refactoring keine Typprüfung durchführen und vom Compiler abgelehnt werden.

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

Zugegeben, die Benennung der Methode als makeService(und auch mit einem Parameter) sollte ziemlich klar machen, was die Methode tut und dass das Refactoring keine sichere Sache war, aber "lokales Denken" bedeutet, dass Sie nicht suchen müssen bei Namenskonventionen und der Implementierung von, makeServiceum das herauszufinden: Jeder Ausdruck, der nicht mechanisch gemischt werden kann (dedupliziert, faul gemacht, eifrig gemacht, toter Code beseitigt, parallelisiert, verzögert, zwischengespeichert, aus einem Cache gelöscht usw.), ohne das Verhalten zu ändern ( dh ist nicht "rein") sollte als wirksam eingegeben werden.

Thilo
quelle
2

Worauf bezieht sich Stateful Service in diesem Fall?

Meinen Sie damit, dass es einen Nebeneffekt ausführt, wenn ein Objekt erstellt wird? Aus diesem Grund ist es besser, eine Methode zu verwenden, die den Nebeneffekt beim Starten Ihrer Anwendung ausführt. Anstatt es während des Baus laufen zu lassen.

Oder sagen Sie vielleicht, dass es innerhalb des Dienstes einen veränderlichen Zustand hat? Solange der interne veränderbare Zustand nicht freigelegt ist, sollte er in Ordnung sein. Sie müssen nur eine reine (referenziell transparente) Methode bereitstellen, um mit dem Dienst zu kommunizieren.

Um meinen zweiten Punkt zu erweitern:

Angenommen, wir erstellen eine In-Memory-Datenbank.

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, dies muss nicht effektiv sein, da das Gleiche passiert, wenn Sie einen Netzwerkanruf tätigen. Sie müssen jedoch sicherstellen, dass es nur eine Instanz dieser Klasse gibt.

Wenn Sie den Katzeneffekt verwenden Ref, würde ich normalerweise flatMapden Schiedsrichter am Einstiegspunkt ansprechen, damit Ihre Klasse nicht effektiv sein muss.

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, wenn Sie einen gemeinsam genutzten Dienst oder eine Bibliothek schreiben, die von einem statusbehafteten Objekt abhängig ist (z. B. mehrere Parallelitätsprimitive), und Sie nicht möchten, dass Ihre Benutzer sich darum kümmern, was initialisiert werden soll.

Dann muss es ja in einen Effekt eingewickelt werden. Sie könnten so etwas verwenden, Resource[F, MyStatefulService]um sicherzustellen, dass alles richtig geschlossen ist. Oder nur, F[MyStatefulService]wenn es nichts zu schließen gibt.

atl
quelle
"Sie müssen nur eine Methode bereitstellen, eine reine Methode, um mit dem Dienst zu kommunizieren." Oder vielleicht genau das Gegenteil: Die anfängliche Konstruktion eines rein internen Zustands muss keine Auswirkung haben, sondern jede Operation auf dem Dienst, die mit diesem veränderlichen Zustand in interagiert Jeder Weg muss dann als wirksam markiert werden (um Unfälle wie val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)) zu vermeiden
Thilo
Oder von der anderen Seite kommen: Ob Sie diese Serviceerstellung effektiv machen oder nicht, ist nicht wirklich wichtig. Unabhängig davon, in welche Richtung Sie gehen, muss die Interaktion mit diesem Dienst in irgendeiner Weise effektiv sein (da er einen veränderlichen Zustand enthält, der von diesen Interaktionen beeinflusst wird).
Thilo
1
@thilo Ja, du hast recht. Was ich damit gemeint habepure dass es referenziell transparent sein muss. Betrachten Sie zB ein Beispiel mit Future. val x = Future {... }und def x = Future { ... }bedeutet etwas anderes. (Dies kann Sie beißen, wenn Sie Ihren Code umgestalten.) Dies ist jedoch bei Katzeneffekten, Monix oder Zio nicht der Fall.
atl