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í?
scala
functional-programming
scala-cats
cats-effect
Mark Canlas
fuente
fuente
delay
devolver un F [Servicio] . Como ejemplo, vea elstart
método en IO , devuelve un IO [Fiber [IO,?]] , En lugar de la fibra normal .Respuestas:
Si ya está utilizando un sistema de efectos, lo más probable es que tenga un
Ref
tipo para encapsular de forma segura el estado mutable.Por eso digo: modelar objetos con estado con
Ref
. 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?Si su pregunta puede reformularse como
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:
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
makeService
efectivo, la refactorización no se verificaría por tipo y sería rechazada por el compilador.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 demakeService
para 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.fuente
¿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.
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
Ref
efecto de gatos, lo que normalmente haría, es alflatMap
árbitro en el punto de entrada, por lo que su clase no tiene que ser efectiva.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 simplementeF[MyStatefulService]
si no hay nada que cerrar.fuente
val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
)pure
es que tiene que ser referencialmente transparente. Por ejemplo, considere un ejemplo con Future.val x = Future {... }
ydef x = Future { ... }
significa algo diferente. (Esto puede morderte cuando refactorizas tu código) Pero, no es el caso con efecto gato, monix o zio.