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.