A construção de objetos com estado deve ser modelada com um tipo de efeito?

9

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?

Mark Canlas
fonte
2
Sim, a criação de um estado mutável é um efeito colateral. Como tal, deve ocorrer dentro de delaye retornar um F [Serviço] . Como um exemplo, veja o startmétodo no IO , ele retorna um IO [Fiber [IO,?]] , Em vez da fibra simples .
Luis Miguel Mejía Suárez
11
Para uma resposta completa para este problema, consulte isto e isto .
Luis Miguel Mejía Suárez

Respostas:

3

A construção de objetos com estado deve ser modelada com um tipo de efeito?

Se você já estiver usando um sistema de efeitos, provavelmente terá um Reftipo para encapsular com segurança o estado mutável.

Então eu digo: modele objetos com estadoRef . 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?


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?

Se sua pergunta puder ser reformulada como

Os benefícios adicionais (além de uma implementação que funciona corretamente usando uma "classe de letra mais fraca") da transparência referencial e do raciocínio local são suficientes para justificar o uso de um tipo de efeito (que já deve estar em uso para acesso e mutação de estado) também para o estado criação ?

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:

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

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 makeServiceeficaz, a refatoração não faria a verificação de tipo e seria rejeitada pelo compilador.

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

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 de makeServicepara 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.

Thilo
fonte
2

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.

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

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 Refefeitos de gatos, o que eu normalmente faria é flatMapo juiz no ponto de entrada, para que sua turma não precise ser eficaz.

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 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 apenas F[MyStatefulService]se não houver nada para fechar.

atl
fonte
"Você só precisa fornecer um método, um método puro para se comunicar com o serviço". Ou talvez seja o contrário: a construção inicial de um estado puramente interno não precisa ser um efeito, mas qualquer operação no serviço que interaja com esse estado mutável no qualquer maneira, então, precisa ser marcada effectful (a acidentes evitar como val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo
Ou vindo do outro lado: se você torna essa criação de serviço eficaz ou não, isso não é realmente importante. Mas não importa para que lado você vá, interagir com esse serviço de qualquer maneira deve ser eficaz (porque ele carrega um estado mutável no interior que será impactado por essas interações).
Thilo #
11
@ thilo Sim, você está certo. O que eu quis dizer com pureisso é que deve ser referencialmente transparente. por exemplo, considere um exemplo com o futuro. val x = Future {... }e def 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.
atl