La programación funcional en Scala puede ser difícil de dominar debido a algunas características sintácticas y semánticas del lenguaje. En particular, algunas de las herramientas de lenguaje y formas de implementar lo que ha planeado con la ayuda de las bibliotecas principales parecen obvias cuando está familiarizado con ellas, pero al comienzo del estudio, especialmente por su cuenta, no es tan fácil reconocerlas.
Por esta razón, decidí que sería útil compartir algunos consejos de programación funcional en Scala. Los ejemplos y los nombres corresponden a los gatos, pero la sintaxis en scalaz debería ser similar debido a la base teórica general.

9) Constructores de métodos de extensión
Comencemos con, quizás, la herramienta más básica: métodos de extensión de cualquier tipo que conviertan una instancia en Opción, Cualquiera, etc., en particular:
.some
y el método correspondiente del constructor none
para Option
;.asRight
, .asLeft
for Either
;.valid
, .invalid
, .validNel
, .invalidNel
para Validated
Dos ventajas principales de su uso:
- Es más compacto y comprensible (ya que se guarda la secuencia de llamadas a métodos).
- A diferencia de las opciones de constructor, los tipos de retorno de estos métodos se extienden a un supertipo, es decir:
import cats.implicits._ Some("a")
Aunque la inferencia de tipos ha mejorado a lo largo de los años, y la cantidad de posibles situaciones en las que este comportamiento ayuda al programador a mantener la calma ha disminuido, los errores de compilación debido a una mecanografía demasiado especializada todavía son posibles en Scala hoy en día. Muy a menudo, el deseo de golpearse la cabeza contra una mesa surge cuando se trabaja con
Either
(ver
Scala con gatos, capítulo 4.4.2).
Una cosa más sobre el tema:
.asRight
y
.asLeft
todavía tienen un parámetro de tipo más. Por ejemplo,
"1".asRight[Int]
es
Either[Int, String]
. Si no se proporciona este parámetro, el compilador intentará generarlo y no obtendrá
Nothing
. Sin embargo, es más conveniente que proporcionar ambos parámetros cada vez o no proporcionar ninguno, como en el caso de los constructores.
8) Cincuenta sombras *>
El operador *> definido en cualquier método
Apply
(es decir, en
Applicative
,
Monad
, etc.) simplemente significa "procesar el cálculo inicial y reemplazar el resultado con lo que se especifica en el segundo argumento". En el idioma del código (en el caso de
Monad
):
fa.flatMap(_ => fb)
¿Por qué utilizar un operador simbólico oscuro para una operación que no tiene un efecto notable? Al comenzar a utilizar ApplicativeError y / o MonadError, encontrará que la operación retiene el efecto de error para todo el flujo de trabajo. Tome
Either
como ejemplo:
import cats.implicits._ val success1 = "a".asRight[Int] val success2 = "b".asRight[Int] val failure = 400.asLeft[String] success1 *> success2
Como puede ver, incluso en caso de error, el cálculo permanece en cortocircuito. *> lo ayudará con el trabajo con cálculos diferidos en
Monix
,
IO
y similares.
Hay una operación simétrica, <*. Entonces, en el caso del ejemplo anterior:
success1 <* success2
Finalmente, si el uso de símbolos es ajeno a usted, no es necesario recurrir a él. *> Es solo un alias para
productR
, y * <es un alias para
productL
.
Nota
En una conversación personal, Adam Warski (¡gracias, Adam!) Comentó correctamente que además de *> (
productR
) también hay >> de
FlatMapSyntax
. >> se define de la misma manera que
fa.flatMap(_ => fb)
, pero con dos matices:
- se define independientemente de
productR
, y por lo tanto, si por alguna razón el contrato de este método cambia (en teoría, se puede cambiar sin violar las leyes monádicas, pero no estoy seguro acerca de MonadError
), no sufrirá; - Más importante aún, >> tiene un segundo operando llamado por llamada por nombre, es decir
fb: => F[B]
. La diferencia en la semántica se vuelve fundamental si realiza cálculos que pueden conducir a una explosión de la pila.
En base a esto, comencé a usar *> con más frecuencia. De una forma u otra, no se olvide de los factores enumerados anteriormente.
7) ¡Levanta las velas!
Muchos toman tiempo para poner el concepto de
lift
en sus cabezas. Pero cuando tengas éxito, encontrarás que él está en todas partes.
Al igual que muchos términos que se elevan en el aire de la programación funcional, el
lift
proviene de la
teoría de categorías . Trataré de explicar: realizar una operación, cambiar la firma de su tipo para que se relacione directamente con el tipo abstracto F.
En gatos, el ejemplo más simple es
Functor :
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
Esto significa: cambiar esta función para que actúe sobre el tipo dado de functor F.
La función de elevación a menudo es sinónimo de constructores anidados para un tipo dado. Entonces,
EitherT.liftF
es esencialmente
EitherT.right.
Ejemplo de Scaladoc :
import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some)
Cherry on the cake: el
lift
presente en todas partes en la biblioteca estándar de Scala. El ejemplo más popular (y quizás el más útil en el trabajo diario) es
PartialFunction
:
val intMatcher: PartialFunction[Int, String] = { case 1 => "jak się masz!" } val liftedIntMatcher: Int => Option[String] = intMatcher.lift liftedIntMatcher(1)
Ahora podemos pasar a cuestiones más urgentes.
6) mapaN
mapN
es una función auxiliar útil para trabajar con tuplas. Nuevamente, esto no es una novedad, sino un reemplazo para el buen operador anterior
|@|
El es un grito.
Así es como se ve mapN en el caso de una tupla de dos elementos:
En esencia, nos permite mapear valores dentro de una tupla de cualquier F que sea un semigrupo (producto) y un functor (mapa). Entonces
import cats.implicits._ ("a".some, "b".some).mapN(_ ++ _)
Por cierto, no olvides que con los gatos obtienes un mapa y un mapa
leftmap
para las tuplas:
("a".some, List("b","c").mapN(_ ++ _))
Otra función útil
.mapN
es instanciar clases de casos:
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))
Por supuesto, prefiere usar el operador de bucle for para esto, pero mapN evita los transformadores monádicos en casos simples.
import cats.effect.IO import cats.implicits._
Los métodos tienen resultados similares, pero este último prescinde de transformadores monádicos.
5) Anidado
Nested
es esencialmente un doble generalizado de transformadores de mónada. Como su nombre lo indica, le permite realizar operaciones de archivos adjuntos bajo ciertas condiciones. Aquí hay un ejemplo para
.map(_.map( :
import cats.implicits._ import cats.data.Nested val someValue: Option[Either[Int, String]] = "a".asRight.some Nested(someValue).map(_ * 3).value
Además de
Functor
,
Nested
generaliza
Applicative
,
ApplicativeError
y
Traverse
. Información adicional y ejemplos están
aquí .
4) .recover / .recoverWith / .handleError / .handleErrorWith / .valueOr
La programación funcional en Scala tiene mucho que ver con el manejo del efecto de error.
ApplicativeError
y
MonadError
tienen algunos métodos útiles, y puede serle útil descubrir las diferencias sutiles entre los cuatro principales. Entonces, con
ApplicativeError F[A]:
handleError
convierte todos los errores en el punto de llamada a A de acuerdo con la función especificada.recover
actúa de manera similar, pero acepta funciones parciales y, por lo tanto, puede convertir los errores que seleccionó en A.handleErrorWith
es similar a handleError
, pero su resultado debería parecerse a F[A]
, lo que significa que te ayuda a convertir errores.recoverWith
actúa como recuperar, pero también requiere F[A]
como resultado.
Como puede ver, puede limitarse a
handleErrorWith
y
recoverWith
, que cubren todas las funciones posibles. Sin embargo, cada método tiene sus ventajas y es conveniente a su manera.
En general, le aconsejo que se familiarice con la API
ApplicativeError , que es una de las más ricas en gatos y heredada en MonadError, lo que significa que es compatible con
cats.effect.IO
,
monix.Task
, etc.
Hay otro método para
Either/EitherT
,
Validated
e
Ior
:
.valueOr
. Esencialmente, funciona como
.getOrElse
para
Option
, pero es genérico para las clases que contienen algo "a la izquierda".
import cats.implicits._ val failure = 400.asLeft[String] failure.valueOr(code => s"Got error code $code")
3) gatos callejeros
alley-cats es una solución conveniente para dos casos:
- instancias de clases de mosaico que no siguen sus leyes al 100%;
- Tipklassy auxiliar inusual, que se puede utilizar correctamente.
Históricamente, la instancia de mónada para
Try
más popular en este proyecto, porque
Try
, como saben, no cumple con todas las leyes monádicas en términos de errores fatales. Ahora él realmente se presenta a los gatos.
A pesar de esto, le recomiendo que se familiarice con
este módulo , puede parecerle útil.
2) Tratar de manera responsable las importaciones
Debe saber, a partir de la documentación, el libro o de otro lugar, que los gatos usan una jerarquía de importación específica:
cats.x
para
cats.x
básicos (kernel);
cats.data
para tipos de datos como Validado, transformadores de mónada, etc.
cats.syntax.x._ para admitir métodos de extensión para que pueda llamar a sth.asRight, sth.pure, etc.
cats.instances.x.
_ para importar directamente la implementación de varias clases típicas en el ámbito implícito para tipos específicos individuales, de modo que al llamar, por ejemplo, sth.pure, no se produzca el error "implícito no encontrado".
Por supuesto, notó la importación de
cats.implicits._
, que importa toda la sintaxis y todas las instancias de la clase de tipo en ámbito implícito.
En principio, cuando desarrolle con Cats, debe comenzar con una cierta secuencia de importaciones de las Preguntas frecuentes, a saber:
import cats._ import cats.data._ import cats.implicits._
Si conoce mejor la biblioteca, puede combinarla a su gusto. Sigue una regla simple:
cats.syntax.x
proporciona una sintaxis de extensión relacionada con x;cats.instances.x
proporciona clases de instancia.
Por ejemplo, si necesita
.asRight
, que es un método de extensión para
Either
, haga lo siguiente:
import cats.syntax.either._ "a".asRight[Int]
Por otro lado, para obtener
Option.pure
, debe importar
cats.syntax.monad
AND cats.instances.option
:
import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option]
Al optimizar manualmente su importación, limitará los ámbitos implícitos en sus archivos Scala y, por lo tanto, reducirá el tiempo de compilación.
Sin embargo, por favor: no haga esto si no se cumplen las siguientes condiciones:
- ya dominaste bien a los gatos
- su equipo posee la biblioteca al mismo nivel
Por qué Porque:
Esto se debe a que tanto
cats.implicits
como
cats.instances.option
son extensiones de
cats.instances.OptionInstances
. De hecho, importamos su alcance implícito dos veces, de lo que confundimos al compilador.
Además, no hay magia en la jerarquía de las implicidades: esta es una secuencia clara de extensiones de tipo. Solo necesita referirse a la definición de
cats.implicits
y examinar la jerarquía de tipos.
Durante unos 10-20 minutos puedes estudiarlo lo suficiente para evitar problemas como estos: créeme, esta inversión definitivamente valdrá la pena.
1) ¡No te olvides de las actualizaciones de gatos!
Puede pensar que su biblioteca de FP es atemporal, pero de hecho los
cats
y
scalaz
están actualizando activamente. Toma los gatos como ejemplo. Estos son solo los últimos cambios:
Por lo tanto, cuando trabaje con proyectos, no olvide verificar la versión de la biblioteca, leer las notas para las nuevas versiones y actualizarlas a tiempo.