Ao usar um ambiente funcional como Scala e cats-effect
, a construção de objetos com estado deve ser modelada com um tipo de efeito?
// 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)
}
Como a construção não é falível, podemos usar uma classe de letra mais fraca Apply
.
// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
new Service(name).pure[F]
Eu acho que tudo isso é puro e determinístico. Apenas não é referencialmente transparente, pois a instância resultante é diferente a cada vez. Essa é uma boa hora para usar um tipo de efeito? Ou haveria um padrão funcional diferente aqui?
scala
functional-programming
scala-cats
cats-effect
Mark Canlas
fonte
fonte
delay
e retornar um F [Serviço] . Como um exemplo, veja ostart
método no IO , ele retorna um IO [Fiber [IO,?]] , Em vez da fibra simples .Respostas:
Se você já estiver usando um sistema de efeitos, provavelmente terá um
Ref
tipo para encapsular com segurança o estado mutável.Então eu digo: modele objetos com estado
Ref
. Como a criação (e o acesso a) já é um efeito, isso também tornará a criação do serviço automática.Isso limita sua pergunta original.
Se você deseja gerenciar manualmente o estado mutável interno com regularidade
var
, deve certificar-se de que todas as operações que tocam nesse estado são consideradas efeitos (e provavelmente também são feitas com thread thread safe), o que é tedioso e propenso a erros. Isso pode ser feito, e eu concordo com a resposta da @ atl de que você não precisa estritamente tornar eficaz a criação do objeto com estado (contanto que possa viver com a perda da integridade referencial), mas por que não salvar o problema e abraçar as ferramentas do seu sistema de efeitos todo o caminho?Se sua pergunta puder ser reformulada como
então: Sim, absolutamente .
Para dar um exemplo de por que isso é útil:
O seguinte funciona bem, mesmo que a criação do serviço não seja efetivada:
Mas se você refatorar isso da seguinte forma, não receberá um erro em tempo de compilação, mas terá alterado o comportamento e provavelmente terá introduzido um bug. Se você tivesse declarado
makeService
eficaz, a refatoração não faria a verificação de tipo e seria rejeitada pelo compilador.A atribuição do nome do método como
makeService
(e com um parâmetro também) deve deixar bem claro o que o método faz e que a refatoração não era algo seguro, mas "raciocínio local" significa que você não precisa procurar nas convenções de nomenclatura e na implementação demakeService
para descobrir isso: qualquer expressão que não possa ser embaralhada mecanicamente (desduplicada, tornada preguiçosa, ansiosa, eliminada pelo código morto, paralelizada, atrasada, em cache, removida de um cache etc) sem alterar o comportamento ( ie não é "puro") deve ser digitado como eficaz.fonte
A que serviço com referência se refere neste caso?
Você quer dizer que ele executará um efeito colateral quando um objeto for construído? Para isso, uma idéia melhor seria ter um método que execute o efeito colateral quando o aplicativo estiver sendo iniciado. Em vez de executá-lo durante a construção.
Ou talvez você esteja dizendo que ele possui um estado mutável dentro do serviço? Desde que o estado mutável interno não seja exposto, ele deve ficar bem. Você só precisa fornecer um método puro (referencialmente transparente) para se comunicar com o serviço.
Para expandir meu segundo ponto:
Digamos que estamos construindo um db na memória.
Na IMO, isso não precisa ser eficaz, pois o mesmo está acontecendo se você fizer uma chamada de rede. Porém, você precisa garantir que haja apenas uma instância dessa classe.
Se você está usando
Ref
efeitos de gatos, o que eu normalmente faria éflatMap
o juiz no ponto de entrada, para que sua turma não precise ser eficaz.OTOH, se você estiver escrevendo um serviço compartilhado ou uma biblioteca que depende de um objeto com estado (digamos várias primitivas de simultaneidade) e não deseja que seus usuários se importem com o que inicializar.
Então, sim, tem que ser envolvido em um efeito. Você pode usar algo como,
Resource[F, MyStatefulService]
para garantir que tudo esteja fechado corretamente. Ou apenasF[MyStatefulService]
se não houver nada para fechar.fonte
val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
)pure
isso é que deve ser referencialmente transparente. por exemplo, considere um exemplo com o futuro.val x = Future {... }
edef x = Future { ... }
significa uma coisa diferente. (Isso pode morder você quando você refatorar seu código) Mas, não é o caso do efeito de gato, monix ou zio.