Menu
Introduction à la gestion d’erreurs en programmation fonctionnelle. Partie 1 : Scala

Introduction à la gestion d’erreurs en programmation fonctionnelle. Partie 1 : Scala

Par Loïc DESCOTTE

14.01.2019

Chez Kaizen nous aimons le sentiment de sécurité que procure la programmation fonctionnelle.
Un des premiers bénéfices que l’on peut observer dans ce type de programmation est la facilité avec laquelle on peut traiter les erreurs.

La programmation fonctionnelle typée se démarque sur ce sujet de deux manières :

  • en reposant sur des fonctions et des types de retour fiables
  • en utilisant des types facilitant la composition

Par exemple, cette méthode a un type de retour que l’on peut considérer comme « non fiable » :

    public String findName(int personId) throws DatabaseException {
  Person person = database.find(personId);
  if(person != null) {
    return person.getName();
  }
  else return null;
}

La signature nous indique que l’on va récupérer une valeur de type String, mais la méthode peut retourner une référence nulle ou encore ne rien retourner du tout en lançant une exception.

Voyons maintenant comment adresser cette problématique à l’aide de la programmation fonctionnelle. Nous utiliserons le langage Scala dans les exemples qui suivent. Le type Option permet d’éviter de recourir à des références nulles. Option[T] signifie qu’on peut avoir une valeur de type T ou une absence valeur. Option[T] peut prendre deux valeurs : Some[T] si la valeur est présente et None dans le cas contraire.

Exemple de fonction renvoyant une option :

    def findName(personId: Int): Option[String] = ???

Le type Option propose plusieurs opérations intéressantes, comme map, flatMap ou getOrElse.

L’opération map permet de modifier le résultat d’une option, dans le cas où cette option est définie (de type Some). Dans le cas contraire rien ne se produit et on évite les null pointer exceptions. Voici comment on peut implémenter la méthode findName à l’aide de Option et map :

    def findName(personId: Int): Option[String] = {
   //ici database.find renvoie une option
   val person: Option[Person] = database.find(personId)
   person.map(_.name)
}
La méthode flatMap permet de combiner deux options (voir plus bas dans ce post). La méthode getOrElse permet de récupérer la valeur encapsulée en fournissant une valeur par défaut en cas de None :
    val name = findPerson(123).getOrElse("name not found")
On peut également utiliser du pattern matching, c’est à dire préciser le comportement à adopter en fonction des différents cas :
    val name = findPerson(123) match {
  case Some(n) => n
  case None => "name not found"
}
Si l’on souhaite avoir plus de détails sur la cause d’une erreur, on peut utiliser le type Either à la place d’Option. Either[L, R] peut prendre deux valeurs : Left[L] ou Right[R]. Le type Left est utilisé pour contenir les erreurs, le type Right pour les résultats dont le calcul a réussi. On peut adapter l’exemple précédent avec Either :
    def findName(personId: Int): Either[String, String] = {
  val person: Either[String, String] = database.find(personId)
  person.map(_.name)
}

Comme pour Option, map sera appelé uniquement si le traitement a réussi (type Right), si person est de type Left, findName renverra directement ce Left, qui contiendra dans notre cas une valeur de type String représentant l’erreur rencontrée.

On peut également utiliser le pattern matching avec Either :

    val name  = findPerson(123) match {
  case Right(n) => n
  case Left(error) => "name not found because " + error
}

Combiner des résultats

Lorsque l’on a plusieurs résultats de type Option ou plusieurs Either et qu’on veut les combiner pour obtenir un résultat global, on peut utiliser la composition grâce à la méthode flatMap :

    def giveOption(x: Int ): Option[Int] = Some(x + 1)
def giveOption2(x: Int ): Option[Int] = Some(x + 2)
val combined = giveOption(1).flatMap(a => giveOption2(a)) //Some(4)
On peut aussi simplifier le code de cette manière :
    val combined = giveOption(1).flatMap(giveOption2) //Some(4)

Le résultat de giveOption(1) est envoyé dans la fonction giveOption2.

Il existe une syntaxe en Scala appelée « for comprehension » qui permet de combiner ce type de wrapper, c’est à dire les types comportant entre autres choses une méthode flatMap (appelés aussi monades) :

    val combined = for {
  a <- giveOption(1)
  b <- giveOption2(a)
} yield b // Some(4)

Une fois compilé, le code sera équivalent à l’exemple précédent écrit avec flatMap (on parle de sucre syntaxique).

Si les 2 options sont de type Some, leur combinaison donnera un type Some. Si une des deux options est de type None, le résultat sera None.

On peut appliquer la même recette avec Either :

    giveEither(1).flatMap(giveEither2) // Right(4)
Ou :
    val combined = for {
  a <- giveEither(1)
  b <- giveEither2(a)
} yield b

Note : lorsque l’on combine plusieurs options ou either avec flatMap (ou avec un for comprehension), le traitement s’arrête au premier None ou au premier Left rencontré.

Si on a besoin d’accumuler toutes les erreurs, par exemple dans une liste, on peut utiliser le type Validated de la librairie Cats ou d’autres librairies comme Scala Hamsters .

Résultats asynchrones

En Scala, le type Future permet de représenter le résultat d’une opération asynchrone. On peut combiner les valeurs de type Future comme nous l’avons fait avec Option et Either :

    def giveFuture(x: Int): Future[Int] = ??? // long processing
def giveFuture2(x: Int ): Future[Int] = ???

val combined: Future[Int] = for {
  a <- giveFuture(1)
  b <- giveFuture2(a)
} yield b
Il est également possible de combiner des valeurs de type Future[Option] ou Future[Either] à l’aide de wrappers :
    def giveFutureEither(x: Int ): Future[Either[String, Int]] = ???
def giveFutureEither2(x: Int ): Future[Either[String, Int]] = ???

val combined: Future[Either[String, Int]] = (for {
  a <- FutureEither(giveFutureEither(1))
  b <- FutureEither(giveFutureEither2(a))
} yield b).wrapped

Dans cet exemple le wrapper FutureEither provient de la librairie Scala Hamsters.

Tout le monde n’ayant pas la chance de pouvoir utiliser Scala dans son travail au quotidien, nous exploreronsla mise en œuvre de ces pratiques dans les langages Kotlin et Typescript dans la prochaine partie de ce post.

Retour aux articles

C'est à lire...