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:
- É mais compacto e compreensível (já que a sequência de chamadas de método é salva).
- 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")
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
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
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)
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)
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:
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(_ ++ _)
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(_ ++ _))
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._
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
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")
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]
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]
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:
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.