9 consejos para usar la biblioteca Cats en Scala

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:

  1. Es más compacto y comprensible (ya que se guarda la secuencia de llamadas a métodos).
  2. 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") //Some[String] "a".some //Option[String] 

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 //Right(b) success2 *> success1 //Right(a) success1 *> failure //Left(400) failure *> success1 //Left(400) 

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 //Right(a) 

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) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

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) //Some(jak się masz!) liftedIntMatcher(0) //None intMatcher(1) //jak się masz! intMatcher(0) //Exception in thread "main" scala.MatchError: 0 

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:

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

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(_ ++ _) //Some(ab) (List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _)) //List(0, 6, 0, 8, 0, 12, 0, 16) 

Por cierto, no olvides que con los gatos obtienes un mapa y un mapa leftmap para las 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) 

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._ //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) 

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 //Some(Right(aaa)) 

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") //"Got error code 400" 

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] //Right[Int, String](a) 

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] //Some(a) 

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:

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

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.

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


All Articles