Affichage des articles dont le libellé est Programming. Afficher tous les articles
Affichage des articles dont le libellé est Programming. Afficher tous les articles

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

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)

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 !

Impératif vs. fonctionnel ou le comment vs. le quoi

Mon intérêt pour Scala m’a conduit à m’intéresser à la programmation fonctionnelle. J’ai eu une expérience plutôt avec des langages privilégiant le style impératif. Scala permettant à la fois les paradigmes fonctionnel et impératif je me suis donc poser des questions sur les différences fondamentales entre ces deux paradigmes. Je souhaite, dans ce billet, partager avec vous l’une des distinctions que j’ai pu glaner lors mes lectures diverses.

Le cas d'école


Je vais prendre un exemple qui nous servira de cas d’école. Il s’agira de calculer la somme des éléments d’une liste d’entiers.

Implémentation dans le style impératif


Voyons une solution implémentée dans le style impératif:

def sum(xs: List[Int]) = {
  var sum = 0
  for(x <- xs)
    sum += x
  sum
}
En image cela donnne:


Quelques remarques à propos de cette première implémentation:
  • Nous avons effectué plusieurs affections avant d’obtenir le résultat final c’est-à-dire la valeur de la somme des nombres constituant notre liste.
  • sum += x ne retourne pas de valeur mais a pour effet de bord de modifier la valeur contenue dans la variable sum.
  • La mutation est au centre de cette version de notre programme: sum est une variable mutable c’est-à-dire qui peut changer tout au long de l’exécution du programme. La tâche est accomplie selon un ensemble (une séquence) de commandes: à chaque étape des affectations (modifications d’emplacements mémoire) sont effectuées. Si vous êtes comme moi vous devez être en train de vous dire “Ok ok! Mais je ne vois pas d’ordre ici pourquoi tu me parles de style impératif là?!” Laissez-moi vous guider un peu... L’impératif ici se trouve dans le fait que notre code donne des ordres à l’ordinateur en lui disant à chaque étape quelle valeur faut-il affecter à quelle variable : notre programme dit comment calculer la somme des éléments de notre liste.
En résumé dans cette première nous itérons sur la liste, séquençons les modifications et faisons des affectations.


Implémentation dans le style fonctionnel


Voyons maintenant l’implémentation dans le style fonctionnel:

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}

Quelques remarques à propos de cette implémentation:
  • Pas d’affectation
  • La version fonctionnelle consiste à dire à l’ordinateur ce qu’est la somme des éléments de la liste; elle est plus déclarative que la version impérative. Et les programmes écrits dans ce style sont en quelque sorte plus simple car nous n’avons pas à raisonner sur les mutations successives des variables.
  • La récursion: La récursion est omniprésente en programmation fonctionnelle. Au fur et à mesure je fais de la programmation fonctionnelle je me suis rendu compte j’en fais très souvent usage pour remplacer notamment les boucles, et exprimer plus simplement (élégamment) certains algorithmes. Mon propos n’est pas de confondre le style fonctionnel avec la récursion mais de souligner un usage plus fréquent de la récursion dans ce style. Ceci n’est pas étranger à la proximité de la programmation fonctionnelle et des mathématiques. Rappelez-vous de la représentation de la suite de Fibonacci ou de celle d’une suite arithmétique. L’expression mathématique de ces deux “objets” utilisent la récursion.


Quelques mots de conclusion


Dans cet article je me suis focalisé sur un seul aspect différenciant les paradigmes fonctionnels et impératifs. J’espère revenir dans d’autres billets sur d’autres aspects de la programmation fonctionnelle.
Voici en conclusion quelques points saillants que je retiens de mes récents voyages dans l’univers fonctionnel:

  • La programmation fonctionnelle est une façon de penser elle peut se traduire même dans des langages hautement impératifs (qui encouragent le style impératif j’entends). Cependant c’est quand même beaucoup plus facile avec un langage fonctionnel (par exemple Haskell)
  • La programmation fonctionnelle n’encourage pas les effets de bord (side effects)
  • La pensée fonctionnelle m’aide à réduire la portée de mes variables et les effets de bord, à composer plus souvent des fonctionnalités
  • La programmation fonctionnelle promeut la composabilité
  • La programmation fonctionnelle promeut l’immutabilité et déconseille la mutabilité
  • L’impératif est plus proche du fonctionnement de la machine: une variable est une partie de la mémoire, sa valeur change au fur et à mesure du déroulement du programme. En fonctionnel on privilégie l'usage des valeurs qui sont immuables; pensez aux variables au sens mathématique.
  • En impératif les étapes du calcul sont détaillées et l’ordre d’exécution est important.
  • En fonctionnel je dis à la machine ce qu’est une opération et je la laisse décider comment la réaliser. Le niveau d’abstraction fait que je n’ai pas besoin de lui préciser toutes les étapes pour qu’elle arrive à accomplir ce que je veux d’elle.

© Nouhoum TRAORE.. Fourni par Blogger.