9 dicas para usar a biblioteca Cats em Scala

A programação funcional no Scala pode ser difícil de dominar devido a alguns recursos sintáticos e semânticos da linguagem. Em particular, algumas das ferramentas de linguagem e maneiras de implementar o que você planejou com a ajuda das principais bibliotecas parecem óbvias quando você está familiarizado com elas - mas no início do estudo, especialmente por conta própria, não é tão fácil reconhecê-las.

Por esse motivo, decidi que seria útil compartilhar algumas dicas de programação funcional no Scala. Exemplos e nomes correspondem a gatos, mas a sintaxe no escalaz deve ser semelhante devido à base teórica geral.



9) Construtores de métodos de extensão


Vamos começar com, talvez, a ferramenta mais básica - métodos de extensão de qualquer tipo que transformam uma instância em Option, Ou, etc., em particular:

  • .some e o método none construtor correspondente para Option ;
  • .asRight , .asLeft for Either ;
  • .valid , .invalid , .validNel , .invalidNel para Validated

Duas vantagens principais de seu uso:

  1. É mais compacto e compreensível (já que a sequência de chamadas de método é salva).
  2. Diferentemente das opções do construtor, os tipos de retorno desses métodos são estendidos para um supertipo, ou seja:

 import cats.implicits._ Some("a") //Some[String] "a".some //Option[String] 

Embora a inferência de tipo tenha melhorado ao longo dos anos e o número de possíveis situações em que esse comportamento ajude o programador a ficar calmo tenha diminuído, os erros de compilação devido à digitação excessivamente especializada ainda são possíveis no Scala hoje. Muitas vezes, surge o desejo de bater a cabeça em uma mesa ao trabalhar com o Either (consulte Scala com gatos, capítulo 4.4.2).

Mais uma coisa sobre o tópico: .asRight e .asLeft ainda têm mais um parâmetro de tipo. Por exemplo, "1".asRight[Int] é E Either[Int, String] . Se esse parâmetro não for fornecido, o compilador tentará produzi-lo e obterá Nothing . No entanto, é mais conveniente do que fornecer ambos os parâmetros de cada vez ou não, como no caso de construtores.

8) Cinquenta tons *>


O operador *> definido em qualquer método Apply (ou seja, em Applicative , Monad etc.) significa simplesmente "processar o cálculo inicial e substituir o resultado pelo que é especificado no segundo argumento". No idioma do código (no caso da Monad ):

 fa.flatMap(_ => fb) 

Por que usar um operador simbólico obscuro para uma operação que não tem um efeito perceptível? Começando a usar o ApplicativeError e / ou MonadError, você verá que a operação retém o efeito de erro para todo o fluxo de trabalho. Tome Either como exemplo:

 import cats.implicits._ val success1 = "a".asRight[Int] val success2 = "b".asRight[Int] val failure = 400.asLeft[String] success1 *> success2 //Right(b) success2 *> success1 //Right(a) success1 *> failure //Left(400) failure *> success1 //Left(400) 

Como você pode ver, mesmo no caso de um erro, o cálculo permanece em curto-circuito. *> irá ajudá-lo a trabalhar com cálculos adiados em Monix , IO e similares.

Existe uma operação simétrica, <*. Então, no caso do exemplo anterior:

 success1 <* success2 //Right(a) 

Finalmente, se o uso de símbolos é estranho para você, não é necessário recorrer a ele. *> É apenas um alias para productR e * <é um alias para productL .

Nota


Em uma conversa pessoal, Adam Warski (obrigado, Adam!) Observou com razão que, além de *> ( productR ), também existe >> do FlatMapSyntax . >> é definido da mesma maneira que fa.flatMap(_ => fb) , mas com duas nuances:

  • é definido independentemente do productR e, portanto, se por algum motivo o contrato desse método for alterado (teoricamente, ele poderá ser alterado sem violar as leis monádicas, mas não tenho certeza sobre o MonadError ), você não sofrerá;
  • mais importante, >> tem um segundo operando chamado por chamada por nome, ou seja, fb: => F[B] . A diferença na semântica se torna fundamental se você executar cálculos que podem levar a uma explosão na pilha.

Com base nisso, comecei a usar *> com mais frequência. De uma forma ou de outra, não se esqueça dos fatores listados acima.

7) Levante as velas!


Muitos levam tempo para colocar o conceito de lift em suas cabeças. Mas quando você tiver sucesso, descobrirá que ele está em todo lugar.

Como muitos termos que pairavam no ar da programação funcional, o lift veio da teoria das categorias . Vou tentar explicar: faça uma operação, altere a assinatura do tipo para que fique diretamente relacionada ao tipo abstrato F.

Em Gatos, o exemplo mais simples é o Functor :

 def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f) 

Isto significa: altere esta função para que ela atue no tipo de função F.

A função de elevação geralmente é sinônimo de construtores aninhados para um determinado tipo. Portanto, EitherT.liftF é essencialmente EitherT.right. Exemplo do Scaladoc :

 import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

Cereja no bolo: o lift presente em toda a biblioteca padrão da Scala. O exemplo mais popular (e talvez o mais útil no trabalho diário) é PartialFunction :

 val intMatcher: PartialFunction[Int, String] = { case 1 => "jak się masz!" } val liftedIntMatcher: Int => Option[String] = intMatcher.lift liftedIntMatcher(1) //Some(jak się masz!) liftedIntMatcher(0) //None intMatcher(1) //jak się masz! intMatcher(0) //Exception in thread "main" scala.MatchError: 0 

Agora podemos avançar para questões mais prementes.

6) mapN


mapN é uma função auxiliar útil para trabalhar com tuplas. Novamente, isso não é uma novidade, mas um substituto para o bom e velho operador |@| Ele é um grito.

Aqui está a aparência de mapN no caso de uma tupla de dois elementos:

 // where t2: Tuple2[F[A0], F[A1]] def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map2(t2._1, t2._2)(f) 

Em essência, ele permite mapear valores dentro de uma tupla a partir de qualquer F que seja um semigrupo (produto) e um functor (mapa). Então:

 import cats.implicits._ ("a".some, "b".some).mapN(_ ++ _) //Some(ab) (List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _)) //List(0, 6, 0, 8, 0, 12, 0, 16) 

A propósito, não esqueça que, com gatos, você obtém mapa e mapa leftmap para tuplas:

 ("a".some, List("b","c").mapN(_ ++ _)) //won't compile, because outer type is not the same ("a".some, List("b", "c")).leftMap(_.toList).mapN(_ ++ _) //List(ab, ac) 

Outra função útil .mapN é instanciar classes de caso:

 case class Mead(name: String, honeyRatio: Double, agingYears: Double) ("półtorak".some, 0.5.some, 3d.some).mapN(Mead) //Some(Mead(półtorak,0.5,3.0)) 

Obviamente, você prefere usar o operador de loop for para isso, mas o mapN evita transformadores monádicos em casos simples.

 import cats.effect.IO import cats.implicits._ //interchangable with eg Monix's Task type Query[T] = IO[Option[T]] def defineMead(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = (for { name <- OptionT(qName) honeyRatio <- OptionT(qHoneyRatio) agingYears <- OptionT(qAgingYears) } yield Mead(name, honeyRatio, agingYears)).value def defineMead2(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = for { name <- qName honeyRatio <- qHoneyRatio agingYears <- qAgingYears } yield (name, honeyRatio, agingYears).mapN(Mead) 

Os métodos têm resultados semelhantes, mas o último dispensa transformadores monádicos.

5) Aninhado


Nested é essencialmente um duplo generalizado de transformadores de mônada. Como o nome sugere, ele permite executar operações de anexo sob certas condições. Aqui está um exemplo para .map(_.map( :

 import cats.implicits._ import cats.data.Nested val someValue: Option[Either[Int, String]] = "a".asRight.some Nested(someValue).map(_ * 3).value //Some(Right(aaa)) 

Além do Functor , o Nested generaliza Applicative , ApplicativeError e Traverse . Informações e exemplos adicionais estão aqui .

4) .recover / .recoverWith / .handleError / .handleErrorWith / .valueOr


A programação funcional no Scala tem muito a ver com o tratamento do efeito de erro. ApplicativeError e MonadError têm alguns métodos úteis, e pode ser útil descobrir as diferenças sutis entre os quatro principais. Então, com ApplicativeError F[A]:

  • handleError converte todos os erros no ponto de chamada em A de acordo com a função especificada.
  • recover atua de maneira semelhante, mas aceita funções parciais e, portanto, pode converter erros selecionados em A.
  • handleErrorWith é semelhante ao handleError , mas seu resultado deve se parecer com F[A] , o que significa que ajuda a converter erros.
  • recoverWith age como recuperar, mas também requer F[A] como resultado.

Como você pode ver, você pode limitar- handleErrorWith a handleErrorWith e recoverWith , que cobrem todas as funções possíveis. No entanto, cada método tem suas vantagens e é conveniente à sua maneira.

Em geral, aconselho que você se familiarize com a API ApplicativeError , que é uma das mais ricas em gatos e herdada do MonadError - o que significa que é suportada em cats.effect.IO , monix.Task etc.

Há outro método para Either/EitherT , Validated e .valueOr - .valueOr . Essencialmente, ele funciona como .getOrElse for Option , mas é genérico para classes que contêm algo "à esquerda".

 import cats.implicits._ val failure = 400.asLeft[String] failure.valueOr(code => s"Got error code $code") //"Got error code 400" 

3) gatos de rua


gatos de rua é uma solução conveniente para dois casos:

  • instâncias de classes de blocos que não seguem suas leis 100%;
  • Typklassy auxiliar incomum, que pode ser usado corretamente.

Historicamente, a instância de mônada para o Try mais popular neste projeto, porque o Try , como você sabe, não satisfaz todas as leis monádicas em termos de erros fatais. Agora ele é verdadeiramente apresentado aos gatos.

Apesar disso, recomendo que você se familiarize com este módulo , que pode lhe parecer útil.

2) Tratar responsavelmente as importações


Você deve saber - da documentação, do livro ou de outro lugar - que os gatos usam uma hierarquia de importação específica:

cats.x para tipos básicos (kernel);
cats.data para tipos de dados como Validado, transformadores de mônada, etc;
cats.syntax.x._ para oferecer suporte a métodos de extensão para que você possa chamar sth.asRight, sth.pure, etc;
cats.instances.x. _ para importar diretamente a implementação de várias classes para o escopo implícito de tipos concretos individuais, para que, ao chamar, por exemplo, sth.pure, o erro "implícito não encontrado" não ocorra.

Obviamente, você notou a importação de cats.implicits._ , que importa toda a sintaxe e todas as instâncias da classe type no escopo implícito.

Em princípio, ao desenvolver com o Cats, você deve começar com uma certa sequência de importações do FAQ, a saber:

 import cats._ import cats.data._ import cats.implicits._ 

Se você conhece melhor a biblioteca, pode combiná-la com o seu gosto. Siga uma regra simples:

  • cats.syntax.x fornece sintaxe de extensão relacionada a x;
  • cats.instances.x fornece classes de instância.

Por exemplo, se você precisar de .asRight , que é um método de extensão para Either , faça o seguinte:

 import cats.syntax.either._ "a".asRight[Int] //Right[Int, String](a) 

Por outro lado, para obter o Option.pure você precisa importar cats.syntax.monad AND cats.instances.option :

 import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option] //Some(a) 

Ao otimizar manualmente sua importação, você limitará os escopos implícitos nos seus arquivos Scala e, assim, reduzirá o tempo de compilação.

No entanto, por favor: não faça isso se as seguintes condições não forem atendidas:

  • você já domina bem os gatos
  • sua equipe é proprietária da biblioteca no mesmo nível

Porque Porque:

 //  ,   `pure`, //    import cats.implicits._ import cats.instances.option._ "a".pure[Option] //could not find implicit value for parameter F: cats.Applicative[Option] 

Isso ocorre porque cats.implicits e cats.instances.option são extensões de cats.instances.OptionInstances . De fato, importamos seu escopo implícito duas vezes, depois confundimos o compilador.

Além disso, não há mágica na hierarquia de implícitos - esta é uma sequência clara de extensões de tipo. Você só precisa se referir à definição de cats.implicits e examinar a hierarquia de tipos.

Por cerca de 10 a 20 minutos, você pode estudá-lo o suficiente para evitar problemas como esses - acredite, esse investimento definitivamente compensará.

1) Não se esqueça das atualizações dos gatos!


Você pode pensar que sua biblioteca FP é atemporal, mas na verdade cats e scalaz atualizando ativamente. Tome gatos como um exemplo. Aqui estão apenas as alterações mais recentes:


Portanto, ao trabalhar com projetos, não se esqueça de verificar a versão da biblioteca, ler as notas para novas versões e atualizar a tempo.

Source: https://habr.com/ru/post/pt448128/


All Articles