La gestion des erreurs avec scala.util.Try (2/2)

Leave a Comment

Après la découverte des bases de la gestion des erreurs avec scala.util.Try, nous allons, dans cette deuxième partie, explorer des fonctionnalités plus avancées. Nous utiliserons un exemple très simple pour illustrer nos propos : lancer un serveur sur un numéro de port fourni par l'utilisateur. Cette tâche se décompose en deux sous-tâches, la première consiste à convertir l'entrée (input dans le code) de l'utilisateur en un numéro de port et la seconde à démarrer le serveur sur ce port. Commençons par la première :

 
    val input = "..."
    val result = Try { input.toInt }
  
Encapsulons ce bout de code dans une fonction parsePort :
 
    def parsePort(input: String): Try[Int] = Try(input.toInt)
  
Comme nous l'avons vu dans la première partie le type de retour de cette méthode est scala.util.Success ou scala.util.Failure selon que la valeur saisie par l'utilisateur soit un nombre ou non. La méthode ci-dessous teste notre super méthode :
 
  @Test
  def parsePortTest() {
   assertTrue(parsePort("80").isInstanceOf[Success[_]])
   assertTrue(parsePort("toto").isInstanceOf[Failure[_]])
  }
  
Munis de cette méthode attaquons-nous à la deuxième sous-tâche : la transformation du numéro de port en un serveur sur ce port.

Transformer la valeur contenue dans Try

La méthode transform correspond à notre problématique. Voici sa signature :

 
  def transform[U](s: (T) ⇒ Try[U], f: (Throwable) => Try[U]): Try[U]
  
C'est une fonction d'ordre supérieur qui prend deux paramètres :
  • une fonction s qui s'applique lorsque le traitement dans Try a réussi. Dans notre cas cette fonction est définie comme suit :
     
        def handleSuccess = (port: Int) ⇒ Try(SimpleServer(port))
      
  • une fonction f qui s'applique lorsque le traitement a échoué. Nous utiliserons 80 comme numéro de port par défaut en cas d'erreur. Voici comment on la définit :
     
         def handleFailure = (t: Throwable) ⇒ Try(SimpleServer(80))
      
Le type SimpleServer ci-dessus est :
 
  case class SimpleServer(port: Int) {
   def start(): Unit = {
     println("Starting the server on port " + port)
     ...
   }
   ...
  }
  
Et enfin le test du code :
import org.junit.Test
import org.junit.Assert._
import scala.util.{Failure, Success, Try}

@Test
def transformTest() {
  //Transform the result
 def handleSuccess = (port: Int) ⇒ Try(SimpleServer(port))
 def handleFailure = (t: Throwable) ⇒ Try(SimpleServer(80))

 //Normal case : we create the server instance with the specified port
 val normalCase = parsePort("9090").transform(handleSuccess, handleFailure)
 assertEquals(Success(SimpleServer(9090)), normalCase)

 //Erroneous case : we create the server instance with the default port
 val erroneousCase = parsePort("toto") transform (handleSuccess, handleFailure)
 assertEquals(Success(SimpleServer(80)), erroneousCase)
}

Fournir un traitement par défaut en cas d'échec

La méthode recover permet de spécifier une fonction dont l'invocation fournit une valeur dans le cas où l'instance du Try est un objet scala.util.Failure.

def recover[U >: T](f: PartialFunction[Throwable, U]): Try[U]
Vous aurez remarqué que la fonction en question est une fonction partielle, et nous la définissons comme suit en ne prenant en compte que les exceptions de type NumberFormatException. Lorsqu'une telle exception est lancée nous utilisons le port 80.
import org.junit.Test
import org.junit.Assert._
import scala.util.{ Failure, Success, Try }

@Test
def recoverTest() {
 val erroneousCase = parsePort {
   "toto"
 } recover {
   case _: NumberFormatException ⇒ 80 // PartialFunction
 } flatMap { port ⇒
   Success(SimpleServer(port))
 }

 assertEquals(Success(SimpleServer(80)), erroneousCase)
}
So far so good ! Maintenant voyons comment appliquer un prédicat sur le contenu de Try.

Filtrer sur le contenu dans une instance de Try

Après avoir parsé le numéro renseigné par l'utilisateur nous devons d'abord vérifier qu'il n'est pas déjà utilisé par un autre service. La méthode filter sert à cela :

parsePort ("8080")
Nous filtrons le résultat obtenu avec la méthode filter de Try et le prédicat suivant :
def predicate(port: Int): Boolean = port != 8080
La méthode filter a la signature suivante :
def filter(p: (T) ⇒ Boolean): Try[T]
Elle prend un prédicat et transforme le Try en un objet de type Failure si le prédicat n'est pas satisfait. FilterTest résume tout cela :
@Test
def filterTest() {
  val erroneousCase = parsePort {
    "8080"
  } filter {
    predicate(_)
  }
  assertTrue(erroneousCase.isInstanceOf[Failure[_]])
}
No restons pas sur cet échec car la méthode recover est là pour permettre de spécifier une valeur par défaut, par exemple 8081, dans le cas où le filtrage échoue.
@Test
def filterTest() {
  val result = parsePort {
    "8080"
  } filter {
    predicate _
  } recover {
    case _ ⇒ 8081
  } map { port ⇒
    SimpleServer(port)
  }
  assertEquals(Success(SimpleServer(8081)), result)
}
Ce code fait beaucoup de choses. D'abord il parse la chaine "8080" et obtient Success(8080). Ensuite il transforme cet objet en un objet Failure avec la méthode filter car le prédicat n'est pas satisfait. La méthode recover fournit 8081 comme valeur par défaut et enfin map crée le serveur avec ce port.

Et pour la route

Ici prend fin notre découverte de scala.util.Try qui offre une API élégante pour gérer des traitements pouvant échouer. À ce point vous en savez assez pour aller plus loin.

La gestion des erreurs avec scala.util.Try (1/2)

Leave a Comment

Scala 2.10 introduit une nouvelle structure monadique Try. Cette classe est particulièrement adaptée à la représentation d'un traitement pouvant réussir en renvoyant une valeur ou échouer en lançant une exception. Commerçons par un exemple simple : parser un numéro de port renseigné par un utilisateur sous forme d'une chaîne de caractères.

val input: String = ""
val port = input.toInt

Ce code est assez simple mais il n'est pas sans danger. En effet si l'utilisateur rentre une valeur qui n'est pas un entier la deuxième ligne du code ci-dessus lance une exception de type NumberFormatException. Voyons cela en action :

scala>val input = "8080"
input: String = 8080
scala>input.toInt
res0: Int = 8080

Avec un input qui n'est pas un nombre :

scala>val input = "toto"
input: String = toto
scala>input.toInt
java.lang.NumberFormatException: For input string: "toto"

Maintenant encapsulons le parsing dans un objet Try.

scala>import scala.util.Try
import scala.util.Try
scala>val input = "8080"
input: String = 8080
scala>Try(input.toInt)
res1: scala.util.Try[Int] = Success(8080)
Le résultat est une instance de scala.util.Success, l'une des deux classes concrètes dérivées de Try. La méthode get permet de récupérer la valeur.
scala>res1.get
res3: Int = 8080

Que se passe-t-il si une erreur se produit lors du parsing de la chaîne ?

scala>val input = "toto"
input: String = toto
scala>Try(input.toInt)
res4: scala.util.Try[Int]=Failure(java.lang.NumberFormatException: For input string: "toto")
Cette fois-ci le résultat est une instance de scala.util.Failure, l'autre classe concrète dérivée de Try. Il contient l'exception lancée suite au traitement. L'invocation de la méthode get sur une instance de Failure lance l'exception qu'elle contient. Essayons-le :
scala>res4.get
java.lang.NumberFormatException: For input string: "toto"

Il existe une méthode qui permet de spécifier une valeur par défaut qui est renvoyée en cas d'échec du traitement.

scala>res4.getOrElse(80)
res7: Int = 80

Voici pour les bases avant de voir dans la deuxième partie des exemples plus avancés. Stay tuned !

Subdivisez vos collections avec groupBy

Leave a Comment

Récemment sur l'un de mes projets en Scala j'ai eu besoin de grouper un ensemble d'objets selon un critère donné. Il s'agissait de grouper des catégories selon le nom de leur parent. Voici une version simplifiée du modèle de données :

case class Category(id: String, name: String, parent: Option[ParentInfo])

case class ParentInfo(id: String, name: String)
Je récupère de la base de données l'ensemble des catégories et j'obtiens une collection qui ressemble à ce qui suit :
val categories = Set(
  Category("1", "Immobilier", None), 
  Category("2", "Véhicules", None), 
  Category("3", "Multimédia", None),
  Category("4", "Motos", Some(ParentInfo("2", "Véhicules"))), 
  Category("5", "Equipement auto", Some(ParentInfo("2", "Véhicules"))),   
  Category("6", "Locations", Some(ParentInfo("1", "Immobilier"))),
  Category("7", "Colocations", Some(ParentInfo("1", "Immobilier"))),
  Category("8", "Consoles et jeux vidéo", Some(ParentInfo("3", "Multimédia"))),
  Category("9", "Matériel informatique", Some(ParentInfo("3", "Multimédia")))    
)

Le premier traitement consiste à filtrer les catégories qui n'ont pas de parent :

categories.filter(cat => cat.parent != None)
Exécuté dans le REPL cela donne :
scala> categories.filter(cat => cat.parent != None)
res0: scala.collection.immutable.Set[Category] = Set(Category(6,Locations,Some(ParentInfo(1,Immobilier))), Category(5,Equipement auto,Some(ParentInfo(2,Véhicules))), Category(7,Colocations,Some(ParentInfo(1,Immobilier))), Category(8,Consoles et jeux vidéo,Some(ParentInfo(3,Multimédia))), Category(4,Motos,Some(ParentInfo(2,Véhicules))), Category(9,Matériel informatique,Some(ParentInfo(3,Multimédia))))

Enfin il suffit d'appliquer la méthode groupBy au résultat obtenu mais voyons rapidement la signature de celle-ci :

def groupBy [K] (f: (A) ⇒ K): Map[K, Traversable[A]]

Cette méthode du trait Traversable prend en paramètre une fonction qui produit les clés de groupement des éléments de la collection initiale. Le résultat de groupBy est un Map qui associe une clé, ici le nom du parent de la catégorie, à une collection d'éléments, ici des catégories filles. Tout cela se fait en une ligne grâce à la puissance des collections de Scala :

categories.filter(cat => cat.parent != None).groupBy(cat => cat.parent.get.name)
//On filtre puis on applique groupBy au résultat

Testez-le dans le REPL, ça marche!

categories.filter(cat => cat.parent != None).groupBy(cat => cat.parent.get.name)
res1: scala.collection.immutable.Map[String,scala.collection.immutable.Set[Category]] = Map(Véhicules -> Set(Category(5,Equipement auto,Some(ParentInfo(2,Véhicules))), Category(4,Motos,Some(ParentInfo(2,Véhicules)))), Immobilier -> Set(Category(6,Locations,Some(ParentInfo(1,Immobilier))), Category(7,Colocations,Some(ParentInfo(1,Immobilier)))), Multimédia -> Set(Category(8,Consoles et jeux vidéo,Some(ParentInfo(3,Multimédia))), Category(9,Matériel informatique,Some(ParentInfo(3,Multimédia)))))
© Nouhoum TRAORE.. Fourni par Blogger.