¿Se debe modelar la construcción de objetos con estado con un tipo de efecto?

9

Cuando se utiliza un entorno funcional como Scala y cats-effect, ¿se debe modelar la construcción de objetos con estado con un tipo de efecto?

// 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 construcción no es falible, por lo que podríamos usar una clase de tipo más débil como Apply.

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

Supongo que todos estos son puros y deterministas. Simplemente no es referencialmente transparente ya que la instancia resultante es diferente cada vez. ¿Es ese un buen momento para usar un tipo de efecto? ¿O habría un patrón funcional diferente aquí?

Mark Canlas
fuente
2
Sí, la creación de un estado mutable es un efecto secundario. Como tal, debería suceder dentro de ay delaydevolver un F [Servicio] . Como ejemplo, vea el startmétodo en IO , devuelve un IO [Fiber [IO,?]] , En lugar de la fibra normal .
Luis Miguel Mejía Suárez
1
Para una respuesta completa a este problema, vea esto y esto .
Luis Miguel Mejía Suárez

Respuestas:

3

¿Se debe modelar la construcción de objetos con estado con un tipo de efecto?

Si ya está utilizando un sistema de efectos, lo más probable es que tenga un Reftipo para encapsular de forma segura el estado mutable.

Por eso digo: modelar objetos con estado conRef . Dado que la creación de (así como el acceso a) ya es un efecto, esto también hará que la creación del servicio también sea efectiva.

Esto deja de lado tu pregunta original.

Si desea administrar manualmente el estado mutable interno con regularidad var, debe asegurarse de que todas las operaciones que toquen este estado se consideren efectos (y muy probablemente también sean seguros para subprocesos), lo cual es tedioso y propenso a errores. Esto se puede hacer, y estoy de acuerdo con la respuesta de @ atl de que no tiene que hacer estrictamente efectiva la creación del objeto con estado (siempre y cuando pueda vivir con la pérdida de integridad referencial), pero ¿por qué no ahorrarse el problema y abrazarlo? ¿Las herramientas de su sistema de efectos hasta el final?


Supongo que todos estos son puros y deterministas. Simplemente no es referencialmente transparente ya que la instancia resultante es diferente cada vez. ¿Es ese un buen momento para usar un tipo de efecto?

Si su pregunta puede reformularse como

¿Son los beneficios adicionales (además de una implementación que funciona correctamente utilizando una "clase de tipo más débil") de transparencia referencial y razonamiento local lo suficiente como para justificar el uso de un tipo de efecto (que ya debe estar en uso para el acceso y la mutación del estado) también para el estado creación?

entonces: sí, absolutamente .

Para dar un ejemplo de por qué esto es útil:

Lo siguiente funciona bien, aunque la creación del servicio no tenga efecto:

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

Pero si refactoriza esto como se muestra a continuación, no obtendrá un error en tiempo de compilación, pero habrá cambiado el comportamiento y probablemente habrá introducido un error. Si hubiera declarado makeServiceefectivo, la refactorización no se verificaría por tipo y sería rechazada por el compilador.

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

Concedido a la denominación del método como makeService(y también con un parámetro) debería dejar bastante claro lo que hace el método y que la refactorización no era algo seguro, pero el "razonamiento local" significa que no tiene que buscar en convenciones de nomenclatura y la implementación de makeServicepara resolverlo: cualquier expresión que no se pueda mezclar mecánicamente (deduplicado, perezoso, ansioso, eliminación de código muerto, paralelo, retrasado, almacenado en caché, purgado de un caché, etc.) sin cambiar el comportamiento ( es decir, no es "puro") debe escribirse como eficaz.

Thilo
fuente
2

¿A qué se refiere el servicio con estado en este caso?

¿Quiere decir que ejecutará un efecto secundario cuando se construye un objeto? Para esto, una mejor idea sería tener un método que ejecute el efecto secundario cuando se inicie su aplicación. En lugar de ejecutarlo durante la construcción.

¿O tal vez estás diciendo que tiene un estado mutable dentro del servicio? Mientras el estado mutable interno no esté expuesto, debería estar bien. Solo necesita proporcionar un método puro (referencialmente transparente) para comunicarse con el servicio.

Para ampliar mi segundo punto:

Digamos que estamos construyendo un db en 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)
}

En mi opinión, esto no tiene por qué ser efectivo, ya que sucede lo mismo si realiza una llamada de red. Sin embargo, debe asegurarse de que solo haya una instancia de esta clase.

Si está utilizando el Refefecto de gatos, lo que normalmente haría, es al flatMapárbitro en el punto de entrada, por lo que su clase no tiene que ser efectiva.

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 está escribiendo un servicio compartido o una biblioteca que depende de un objeto con estado (digamos múltiples primitivas de concurrencia) y no desea que a sus usuarios les importe qué inicializar.

Entonces, sí, tiene que estar envuelto en un efecto. Podría usar algo como, Resource[F, MyStatefulService]para asegurarse de que todo esté cerrado correctamente. O simplemente F[MyStatefulService]si no hay nada que cerrar.

atl
fuente
"Solo necesita proporcionar un método, un método puro para comunicarse con el servicio" O tal vez todo lo contrario: la construcción inicial del estado puramente interno no necesita ser un efecto, sino cualquier operación en el servicio que interactúe con ese estado mutable en de cualquier manera, entonces debe ser marcado como efectivo (para evitar accidentes como val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
O viniendo del otro lado: si haces que la creación del servicio sea efectiva o no, no es realmente importante. Pero no importa en qué dirección se encuentre, interactuar con ese servicio de cualquier manera debe ser efectivo (porque lleva dentro un estado mutable que se verá afectado por estas interacciones).
Thilo
1
@thilo Sí, tienes razón. Lo que quise decir purees que tiene que ser referencialmente transparente. Por ejemplo, considere un ejemplo con Future. val x = Future {... }y def x = Future { ... }significa algo diferente. (Esto puede morderte cuando refactorizas tu código) Pero, no es el caso con efecto gato, monix o zio.
atl