9 conseils pour utiliser la bibliothèque Cats à Scala

La programmation fonctionnelle dans Scala peut être difficile à maîtriser en raison de certaines caractéristiques syntaxiques et sémantiques du langage. En particulier, certains des outils linguistiques et des moyens de mettre en œuvre ce que vous avez planifié avec l'aide des bibliothèques principales semblent évidents lorsque vous les connaissez - mais au tout début des études, en particulier par vous-même, il n'est pas si facile de les reconnaître.

Pour cette raison, j'ai décidé qu'il serait utile de partager quelques conseils de programmation fonctionnelle dans Scala. Les exemples et les noms correspondent aux chats, mais la syntaxe dans scalaz devrait être similaire en raison de la base théorique générale.



9) Constructeurs de méthode d'extension


Commençons par, peut-être, l'outil le plus basique - les méthodes d'extension de tout type qui transforment une instance en Option, Soit, etc., en particulier:

  • .some et la méthode correspondante du constructeur none pour Option ;
  • .asRight , .asLeft pour Either ;
  • .valid , .invalid , .validNel , .invalidNel pour Validated

Deux principaux avantages de leur utilisation:

  1. Il est plus compact et plus compréhensible (puisque la séquence d'appels de méthode est enregistrée).
  2. Contrairement aux options de constructeur, les types de retour de ces méthodes sont étendus à un supertype, à savoir:

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

Bien que l'inférence de type se soit améliorée au fil des ans et que le nombre de situations possibles dans lesquelles ce comportement aide le programmeur à rester calme a diminué, des erreurs de compilation dues à une frappe trop spécialisée sont toujours possibles dans Scala aujourd'hui. Très souvent, le désir de se cogner la tête contre une table se manifeste lorsque vous travaillez avec Either (voir Scala avec les chats, chapitre 4.4.2).

Une dernière chose sur le sujet: .asRight et .asLeft ont encore un paramètre de type supplémentaire. Par exemple, "1".asRight[Int] est Either[Int, String] . Si ce paramètre n'est pas fourni, le compilateur essaiera de le sortir et n'obtiendra Nothing . Néanmoins, c'est plus pratique que de fournir les deux paramètres à chaque fois ou de ne pas en fournir non plus, comme dans le cas des constructeurs.

8) Cinquante teintes *>


L'opérateur *> défini dans n'importe quelle méthode Apply (c'est-à-dire dans Applicative , Monad , etc.) signifie simplement «traiter le calcul initial et remplacer le résultat par ce qui est spécifié dans le deuxième argument». Dans la langue du code (dans le cas de Monad ):

 fa.flatMap(_ => fb) 

Pourquoi utiliser un opérateur symbolique obscur pour une opération qui n'a pas d'effet notable? En commençant à utiliser ApplicativeError et / ou MonadError, vous constaterez que l'opération conserve l'effet d'erreur pour l'ensemble du flux de travail. Prenez Either comme exemple:

 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) 

Comme vous pouvez le constater, même en cas d'erreur, le calcul reste court-circuité. *> vous aidera à travailler avec des calculs différés dans les Monix , IO et similaires.

Il y a une opération symétrique, <*. Donc, dans le cas de l'exemple précédent:

 success1 <* success2 //Right(a) 

Enfin, si l'utilisation des symboles vous est étrangère, il n'est pas nécessaire d'y recourir. *> Est juste un alias pour productR , et * <est un alias pour productL .

Remarque


Dans une conversation personnelle, Adam Warski (merci, Adam!) A fait remarquer à juste titre qu'en plus de *> ( productR ) il y a aussi >> de FlatMapSyntax . >> est défini de la même manière que fa.flatMap(_ => fb) , mais avec deux nuances:

  • il est défini indépendamment de productR , et donc, si pour une raison quelconque le contrat de cette méthode change (théoriquement, il peut être changé sans violer les lois monadiques, mais je ne suis pas sûr de MonadError ), vous ne souffrirez pas;
  • plus important encore, >> a un deuxième opérande appelé par appel par nom, c'est-à-dire fb: => F[B] . La différence de sémantique devient fondamentale si vous effectuez des calculs qui peuvent conduire à une explosion de pile.

Sur cette base, j'ai commencé à utiliser *> plus souvent. D'une manière ou d'une autre, n'oubliez pas les facteurs énumérés ci-dessus.

7) Levez les voiles!


Beaucoup prennent le temps de mettre le concept d' lift dans leur tête. Mais quand vous réussirez, vous constaterez qu'il est partout.

Comme de nombreux termes planant dans l'air de la programmation fonctionnelle, l' lift est venu de la théorie des catégories . Je vais essayer d'expliquer: prendre une opération, changer la signature de son type pour qu'elle soit directement liée au type abstrait F.

Dans Cats, l'exemple le plus simple est Functor :

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

Cela signifie: modifiez cette fonction pour qu'elle agisse sur le type de foncteur F.

La fonction lift est souvent synonyme de constructeurs imbriqués pour un type donné. Ainsi, EitherT.liftF est essentiellement EitherT.right. Exemple de Scaladoc :

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

Cerise sur le gâteau: l' lift présent partout dans la bibliothèque standard de Scala. L'exemple le plus populaire (et peut-être le plus utile dans le travail quotidien) est 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 

Nous pouvons maintenant passer à des questions plus urgentes.

6) mapN


mapN est une fonction d'aide utile pour travailler avec des tuples. Encore une fois, ce n'est pas une nouveauté, mais un remplacement pour le bon vieil opérateur |@| C'est un cri.

Voici à quoi ressemble mapN dans le cas d'un tuple de deux éléments:

 // 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 substance, cela nous permet de mapper des valeurs à l'intérieur d'un tuple de n'importe quel F qui sont un semi-groupe (produit) et un foncteur (carte). Donc:

 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) 

Soit dit en passant, n'oubliez pas qu'avec les chats, vous obtenez une carte et une carte de leftmap pour les tuples:

 ("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) 

Une autre fonction .mapN utile .mapN instancier des classes de cas:

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

Bien sûr, vous utilisez plutôt l'opérateur de boucle for pour cela, mais mapN évite les transformateurs monadiques dans les cas 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) 

Les méthodes ont des résultats similaires, mais cette dernière se passe de transformateurs monadiques.

5) imbriqué


Nested est essentiellement un double généralisé de transformateurs monades. Comme son nom l'indique, il vous permet d'effectuer des opérations de pièce jointe sous certaines conditions. Voici un exemple pour .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)) 

En plus de Functor , Functor généralise Applicative , ApplicativeError et Traverse . Des informations supplémentaires et des exemples sont ici .

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


La programmation fonctionnelle dans Scala a beaucoup à voir avec la gestion de l'effet d'erreur. ApplicativeError et MonadError ont quelques méthodes utiles, et il peut être utile pour vous de découvrir les différences subtiles entre les quatre principales. Donc, avec ApplicativeError F[A]:

  • handleError convertit toutes les erreurs au point d'appel en A selon la fonction spécifiée.
  • recover actes d'une manière similaire, mais accepte des fonctions partielles, et peut donc convertir les erreurs que vous avez sélectionnées en A.
  • handleErrorWith est similaire à handleError , mais son résultat devrait ressembler à F[A] , ce qui signifie qu'il vous aide à convertir les erreurs.
  • recoverWith agit comme récupérer, mais nécessite également F[A] comme résultat.

Comme vous pouvez le voir, vous pouvez vous limiter à handleErrorWith et recoverWith , qui couvrent toutes les fonctions possibles. Cependant, chaque méthode a ses avantages et est pratique à sa manière.

En général, je vous conseille de vous familiariser avec l'API ApplicativeError , qui est l'une des plus riches en chats et héritée de MonadError - ce qui signifie qu'elle est prise en charge dans cats.effect.IO , monix.Task , etc.

Il existe une autre méthode pour Either/EitherT , Validated et Ior - .valueOr . Essentiellement, il fonctionne comme .getOrElse pour Option , mais est générique pour les classes contenant quelque chose «à gauche».

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

3) chats de ruelle


alley-cats est une solution pratique pour deux cas:

  • instances de classes de tuiles qui ne respectent pas leurs lois à 100%;
  • Typklassy auxiliaire inhabituel, qui peut être utilisé correctement.

Historiquement, l'instance de monade pour Try plus populaire de ce projet, car Try , comme vous le savez, ne satisfait pas à toutes les lois monadiques en termes d'erreurs fatales. Maintenant, il est vraiment présenté aux chats.

Malgré cela, je vous recommande de vous familiariser avec ce module , il peut vous sembler utile.

2) Traiter de manière responsable les importations


Vous devez savoir - à partir de la documentation, du livre ou d'ailleurs - que les chats utilisent une hiérarchie d'importation spécifique:

cats.x pour les cats.x de base (noyau);
cats.data pour les types de données comme Validated, transformateurs cats.data , etc.;
cats.syntax.x._ pour prendre en charge les méthodes d'extension afin que vous puissiez appeler sth.asRight, sth.pure, etc.
cats.instances.x. _ d'importer directement l'implémentation de diverses classes de types dans la portée implicite de types concrets individuels afin que lors de l'appel, par exemple, sth.pure, l'erreur "implicite introuvable" ne se produise pas.

Bien sûr, vous avez remarqué l'importation de cats.implicits._ , qui importe toute la syntaxe et toutes les instances de la classe type dans une portée implicite.

En principe, lors du développement avec Cats, vous devez commencer par une certaine séquence d'importations de la FAQ, à savoir:

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

Si vous apprenez à mieux connaître la bibliothèque, vous pouvez la combiner à votre goût. Suivez une règle simple:

  • cats.syntax.x fournit une syntaxe d'extension liée à x;
  • cats.instances.x fournit des classes d'instance.

Par exemple, si vous avez besoin de .asRight , qui est une méthode d'extension pour Either , procédez comme suit:

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

En revanche, pour obtenir Option.pure vous devez importer cats.syntax.monad ET cats.instances.option :

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

En optimisant manuellement votre importation, vous limiterez les étendues implicites dans vos fichiers Scala et réduirez ainsi le temps de compilation.

Cependant, veuillez ne pas le faire si les conditions suivantes ne sont pas remplies:

  • vous avez déjà bien maîtrisé les chats
  • votre équipe possède la bibliothèque au même niveau

Pourquoi? Parce que:

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

En effet, les cats.implicits et cats.instances.option sont des extensions de cats.instances.OptionInstances . En fait, nous importons sa portée implicite deux fois, que nous confondons le compilateur.

De plus, il n'y a pas de magie dans la hiérarchie des implicites - c'est une séquence claire d'extensions de type. Il vous suffit de vous référer à la définition de cats.implicits et d'examiner la hiérarchie des types.

Pendant 10 à 20 minutes, vous pouvez l'étudier suffisamment pour éviter de tels problèmes - croyez-moi, cet investissement sera certainement payant.

1) N'oubliez pas les mises à jour des chats!


Vous pourriez penser que votre bibliothèque FP est intemporelle, mais en fait, les cats et les scalaz à jour activement. Prenons l'exemple des chats. Voici juste les derniers changements:


Par conséquent, lorsque vous travaillez avec des projets, n'oubliez pas de vérifier la version de la bibliothèque, de lire les notes des nouvelles versions et de les mettre à jour à temps.

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


All Articles