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:
- Il est plus compact et plus compréhensible (puisque la séquence d'appels de méthode est enregistrée).
- Contrairement aux options de constructeur, les types de retour de ces méthodes sont étendus à un supertype, à savoir:
import cats.implicits._ Some("a")
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
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
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)
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)
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:
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(_ ++ _)
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(_ ++ _))
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._
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
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")
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]
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]
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:
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.