Première approche de la programmation fonctionnelle avec Arrow-kt sur Android

Julien Bouffard
L’Actualité Tech — Blog CBTW
7 min readJan 27, 2022

L’objectif de cet article n’est pas de partir sur de la théorie et la définition d’un monad, d’un functor, … Mais de tester certains concepts de façon pragmatique, en utilisant la programmation fonctionnelle sur un projet Android avec Kotlin.

Programmation fonctionnelle — Photo by Luca Bravo

La théorie reste évidemment essentielle pour bien comprendre la programmation fonctionnelle, mais il s’agit ici d’une première approche pour appréhender les bénéfices de celle-ci.

Arrow Kt est une librairie Kotlin qui augmente les capacités du langage pour gérer la programmation fonctionnelle. La librairie est décomposée en 4 sous-parties. Nous n’utiliserons que la partie Core dans cet article.

Pour info, il existe également les parties Fx, Optics et Analysis.

Un tout petit peu de théorie

Pas beaucoup promis !
Les concepts abordés seront :

  • Either
  • Validated
  • Option

Avec ces concepts-là, nous pouvons déjà répondre à de nombreuses problématiques rencontrées au quotidien dans le développement.

Either

Either comme son nom l’indique renvoie soit une valeur, soit une autre. Il est couramment utilisé pour mapper un retour en erreur ou en succès : Either<Error, SuccessResult>. Il n’est pas interdit de mapper autre chose à la place d’Error, mais par convention dans les langages orientés fonctionnels, la partie droite d’Either contient toujours les valeurs en succès.

Il est déjà courant d’utiliser ce pattern avec une sealed class Result sans avoir recours au FP. L’avantage est qu’Either va permettre de composer de multiples Either sans avoir à vérifier à chaque étape que nous avons une valeur en succès.

On compose nos calculs, requêtes HTTP, … avec flatmap, on renvoie une instance d’Either (que ce soit Left dans le cas d’une erreur ou Right en cas de succès). Au final on map pour obtenir la valeur en cas de succès ou mapLeft si on veut faire quelque chose avec l’erreur. Le deuxième flatMap n’est pas calculé.

Dans le premier test, on enchaîne les calculs en succès. Et en bout de chaîne on teste le résultat.

Dans le deuxième test, le premier flatmap tombe en erreur. Au lieu d’avoir à vérifier qu’on ait un Result en erreur si on utilise ce pattern ou un catch si on utilise les exceptions, on court-circuite tout le process et on arrive directement au mapLeft. Rx a un fonctionnement similaire, d’ailleurs Rx emprunte de nombreux concepts à la programmation fonctionnelle.

Il est courant dans notre code que des fonctions partent en erreur (un appel http, une query à une base de données, une validation métier). Nous avons l’habitude en OOP de lancer des exceptions dans ce cas. Il faut toujours les catcher au bon endroit pour éviter de crasher l’app. Comment communiquer l’erreur ? En la rendant explicite dans la signature de retour de notre fonction.

Validated

Validated suit le même principe qu’Either mais permet d’accumuler les erreurs tandis qu’Either est utilisé pour court circuiter la procédure à la première erreur.

Option

Option est un concept inclut dans Arrow. Il représente la possibilité d’une valeur. Option peut être soit de type Some qui contient une valeur (une liste de user, un panier d’achat, etc), soit de type None pour signifier qu’il n’y a pas de valeur.

Arrow propose Option mais conseille de ne l’utiliser qu’en cas de réel besoin car Kotlin propose déjà au sein du langage les variables nullable qui remplissent globalement les mêmes fonctions. Par souci de performance, Option n’est donc pas tant utilisé que ça. Pour cette première approche, je n’ai d’ailleurs pas ressenti le besoin de l’utiliser et j’ai privilégié la variable nullable.

Mise en pratique

Pour tester la programmation fonctionnelle avec Arrow-kt, nous allons faire une application Android qui permet de suivre les scores de parties de pétanque avec une base de données locale.

L’installation se fait simplement via gradle :

implementation "io.arrow-kt:arrow-core:$arrow_version"

Ci-dessous la classe métier qui va réunir toutes les règles de validation sur les étapes d’une partie de pétanque.

  • La méthode startGame va créer la nouvelle partie et la sauvegarder ou renvoyer une liste d’erreurs.
  • la méthode endRound va ajouter le score obtenu par le gagnant de la manche à son total, sauvegarder en base de données et renvoyer l’équipe si celle-ci a gagné la partie ou renvoyer une erreur s’il y en a qui surgit durant le process.
  • la méthode endGame clôture la partie.
Game.kt class

Deux approchent ressortent. La méthode startGame va cumuler les erreurs puis les renvoyer, les méthodes endRound et endGame renvoient directement l’erreur dès qu’il y en a une qui survient.

Méthode startGame

Ci-dessous toutes les validations de la méthode : on renvoie une instance de ValidedNel (c’est une instance de Validated avec une instance de NonEmptyList) à chaque validation. Si tout est ok, on renvoie un validNel avec l’instance de Game, sinon un invalidNel avec une instance de GameError (erreur métier custom).

La signature de retour de ses méthodes de validation est ValidatedNel<GameError, Game>, donc une liste d’erreurs ou une liste de parties.

StartGameValidation.kt

Revenons à notre méthode startGame. On zip toutes les validations (on pourrait en avoir plus) avec une liste non vide.

Au final, si toutes les validations renvoient un validNel, on récupère la partie et on l’enregistre en base de données locale via notre repository Room qui renvoie un Either<Throwable, Game>.

Si une ou plusieurs validations renvoient un invalidNel, on atterrit dans la méthode handleErrorWith qui porte en paramètre une NonEmptyList<GameError>. On map et transforme le résultat en Either<Nel<GameError>, Either<Throwable, Game>>.

Si nous rencontrons des erreurs de validation, notre retour contiendra une liste d’erreurs. Dans le cas contraire, on récupère un Either avec la partie de pétanque enregistrée en base de données ou l’erreur si Room a rencontré une erreur.

Pas d’exception à catcher ici. On peut afficher, logger, transformer toute la chaîne de calcul au fur et à mesure.

Méthode endRound

Le but de la méthode est de renvoyer soit un gagnant si une équipe à le nombre de points requis pour gagner la partie, ou null s’il n’y a pas encore de vainqueur, ou une GameError.

Dans cette méthode, on renvoie une erreur dès qu’on en rencontre une, il n’y a pas d’accumulation d’erreurs.

La signature de retour des validations est donc ici Either<GameError, Unit> et non ValidatedNel<GameError, Game> comme dans l’autre méthode.

On remarque le DSL either {} en début de méthode qui permet de coder de façon impérative plutôt que d’enchaîner les flatmap sur les Either.

À chaque appel d’une fonction retournant un Either, on ajoute bind() pour que le mapping se fasse sur la droite (valeur en succès). Si le retour est gauche, ça court-circuite toute la méthode et renvoie un Either.left.

On utilise le repository pour récupérer l’équipe de la base de données, on lui ajoute son nombre de points de la manche et on l’enregistre en base de données. À la fin, on vérifie si l’équipe a gagné la partie. Si le repository nous renvoie une erreur pour quelques raisons que ce soit (connexion, donnée invalide), on map l’erreur vers une erreur typée.

Tester avec arrow

Tester nos fonctions se révèle simple et lisible. Prenons deux tests pour la méthode startGame et la méthode endRound.

  • test different valid teams should start the game

On utilise either{} pour contenir notre test. Étant donné que la signature de retour de startGame est Either<Nel<GameError>, Either<Throwable, Game>>, la partie gauche de either peut-être Nel<GameError> ou Throwable donc on type par Any pour englober ces deux possibilités (il y a certainement possibilité d’améliorer cela). Notre assert ne renvoie aucune valeur, la partie droite est Unit.

Le premier bind() va retourner la première partie de notre retour de méthode, à savoir soit Nel<GameError> soit Either<Throwable, Game>.

Le deuxième bind() va retourner soit Throwable, soit Game. Dans ce test sur le happy path, on récupère une instance Game sur laquelle on fait nos assertions comme d’habitude.

  • test multiple invalid game setup should return list of errors

Dans ce test, on veut tester une erreur de validation qui retourne Nel<GameError>. Notre DSL either est typé à gauche avec Nel<GameError> car on ne peut avoir que ce type retour gauche. De ce fait le bind() va court-circuiter et retourner un Either.left(Nel<GameError>) sur lequel on mapLeft pour utiliser le retour de gauche afin de faire nos assertions.

  • test team has won round and became winner should return winner

Dans ce test happy path de la méthode endRound, les 2 premiers bind() vont retourner le résultat de startGame. Avec l’instance de Game retournée, on peut bind() sur la méthode endRound et tester nos assertions.

  • test save in repository trigger exception should fail end round

On attend ici un Either.left(CannotEndRound) en retour de la méthode endRound. Pour exécuter nos assertions sur la partie de gauche, on ne doit pas court-circuiter notre Either. On utilise donc mapLeft pour récupérer la valeur de gauche.

Ces DSL either et bind permettent d’écrire notre code de façon impérative au lieu d’imbriquer les flatMap et map qui rendraient notre code illisible.
La façon de programmer n’est pas vraiment différente de celle de production. Il n’y a pas de hack pour tester.

Et pour le code non fonctionnel ?

Quand est-il de notre code quand celui ci n’est pas configuré pour le FP, comme Room ?
Pas de problème, Arrow permet de transformer notre retour en Either. On peut désormais composer notre retour avec d’autres fonctions.

Conclusion

Cet article a pour volonté de mettre en pratique quelques concepts de la programmation fonctionnelle sur Kotlin avec l’aide de la librairie Arrow kt.

On effleure ici certains concepts, et d’autres parties de la librairie ne sont pas abordées. L’ approche n’est donc pas du tout exhaustive, mais elle donne envie de creuser plus loin.
Il en ressort que la gestion des erreurs peut être plus efficace que dans une approche OOP, même pour un débutant en la matière.
Le fait de pouvoir composer ses fonctions avec un meilleur contrôle sur les paramètres et retours améliore la lisibilité et la robustesse de nos applications.
Les bénéfices d’utilisation de la programmation fonctionnelle avec Kotlin grâce à Arrow kt sont donc à mon sens rapidement apparents.

Nous publions régulièrement des articles sur des sujets de développement produit web et mobile, data et analytics, sécurité, cloud, hyperautomatisation et digital workplace.
Suivez-nous pour être notifié des prochains articles et réaliser votre veille professionnelle.

Retrouvez aussi nos publications et notre actualité via notre newsletter, ainsi que nos différents réseaux sociaux : LinkedIn, Twitter, Youtube, Twitch et Instagram

Vous souhaitez en savoir plus ? Consultez notre site web et nos offres d’emploi.

--

--