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

La backpressure du pauvre avec Akka et les futures

Il m’est souvent arrivé d’utiliser des acteurs Akka pour interroger des services tiers comme un index Elasticsearch ou une base de données. Je me suis trouvé dans des situations où le service est submergé par les requêtes de l’acteur consommateur. La solution facile consiste à bloquer sur chaque appel au service et à lui envoyer une nouvelle requête seulement s’il a répondu à la précédente. Soit la méthode pour évoquer le service :

 
def callService: Future[Response] = ???

Dans le code cela se traduit comme suit :

 
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._

val response = Await.result(callService, 3 seconds) 

Afin de comprendre la solution non bloquante que je vais vous présenter prenons cet exemple. Nous avons un service exposant une API pour récupérer des données en spécifiant un index de départ et le nombre d’entrées que l’on souhaite récupérer. La contrainte est que si on demande un nombre trop important d’entrées d’un coup le service peut se casser la gueule ! De même il peut s’écrouler et ou avoir des temps de réponse trop élevés s’il reçoit trop de requêtes simultanément.

La solution consiste en un système d’acteurs constitué d’un master et de plusieurs workers. Le master crée un ensemble de tâches qu’il fait exécuter par les workers. Une tâche est représentée par la structure suivante :

 
case class Task(from: Int, size: Int)

Au démarrage de l’application le master crée les tâches à exécuter :

 
class Master extends Actor with ActorLogging {
  var tasks = Tasks.empty
  def receive: Receive = {
    case message @ FetchData(from, to) =>
      tasks = Tasks.from(from, to, step)
      context.become(working)
      createAndStartWorkers()
  }
}

J’ai créé une petite structure, Tasks, afin de simplifier la gestion des tâches :

 
case class Tasks(values: Vector[Task]) {
  def isDone = values.isEmpty

  def next: Option[(Task, Tasks)] =
    if(isDone) None
    else Option((values.head, copy(values = values.drop(1))))
}

A la réception d’un message de type Task le worker récupère les données correspondantes :

 
class Worker(master: ActorRef) extends Actor {
  def receive: Receive = {
    case task @ Task(from, size) =>
      fetchData(from, size) onSuccess {
        case data =>  master ! GetTask
      }
  }

  def fetchData(from: Int, size: Int): Future[Data] = ???
}

Une fois que le worker finit de traiter une tâche il notifie le master qui lui assigne une nouvelle tâche ou arrête si tous les workers ont fini de bosser :

 
class Master extends Actor with ActorLogging {
  var tasks = Tasks.empty
  val step = 10
  val numberOfWorkers = 2
  var remainingWorkingWorkers = numberOfWorkers

  def waiting: Receive = {
    case message @ FetchData(from, to) =>
      tasks = Tasks.from(from, to, step)
      context.become(working)
      createAndStartWorkers()
  }

  def receive = waiting

  def working: Receive = {
    case GetTask =>
      tasks.next.fold({
        log.info("No more tasks... ;-)")
        remainingWorkingWorkers = remainingWorkingWorkers - 1
        shutdownIfAllTasksCompleted()
      }) {
        case (task, newTasks) =>
          tasks = newTasks
          sender ! task
      }
  }

  def createAndStartWorkers(): Unit = {
    (0 until numberOfWorkers) foreach(_ => context.actorOf(Props(new Worker(self))) ! StartWorking)
  }

  def allTasksCompleted = remainingWorkingWorkers == 0

  def shutdownIfAllTasksCompleted() =
    if(allTasksCompleted) context.system.shutdown()
}

Tout le code est disponible sur Github : https://github.com/nouhoum/akka-stuff/tree/poor-man-backpressure-akka.

Les fonctions et les fonctions d’ordre supérieur

La programmation fonctionnelle a de nombreux attraits dont le traitement réservé aux fonctions. Dans beaucoup de langages de programmation les fonctions ont un statut particulier. Par exemple en Java il est possible de créer un entier, de l’assigner à une variable ou de le passer comme argument à une autre fonction mais on ne peut pas de même avec les fonctions. En revanche les fonctions jouent un rôle central dans la programmation fonctionnelle et ne sont pas traitées comme des citoyens de seconde zone par les langages dits fonctionnels (Haskell, Caml, Clojure etc.). Cela est important pour l’écriture de certains types d’abstractions bien utiles et courantes en développement comme nous le verrons dans la suite mais avant d’aller plus loin définissons une fonction.

C’est quoi une fonction ?

Ce qu’on entend par fonction varie selon les langages de programmation mais la définition que je vais donner ici est suffisante pour les propos de l’article. Une fonction est une transformation qui produit une valeur à partir d’un ensemble de paramètres, les arguments de la fonction. Les choses seront plus claires avec des exemples. Si nous notons par x un nombre entier quelconque. x peut donc prendre comme valeur 0, 1, 2 etc. nous pouvons définir la fonction f comme suit :

Ne vous laissez pas impressionner par la notation. f est le petit nom donné à la fonction, la lettre x à gauche de la flèche désigne l’argument de la fonction et à droite se trouve sa définition. En donnant une valeur spécifique au paramètre, par exemple 1 nous obtenons :

Si x vaut 3 la valeur retournée par la fonction est :
Au vu de ces valeurs il est aisé de comprendre que cette fonction double la valeur qu’on lui passe en paramètre. Renommons la fonction afin de mieux exprimer cette intention :

Le langage Scala est utilisé dans la suite pour illustrer nos propos. Et la fonction double est codée en Scala comme suit :

def double(number: Int) = 2 * number
Dans le code ci-dessus le mot clé def permet de définir une fonction. Il est suivi du nom de la fonction. J’ai choisi, à la place du paramètre x, un nom plus parlant : number. L’annotation Int (pour Integer) spécifie au compilateur que le paramètre number est un entier. Le reste du code est une traduction parfaite du pseudo code.

Les sorties suivantes de la console interactive (REPL) de Scala permettent de tester notre fonction :

$ scala
scala> def double(number: Int) = 2 * number
double: (number: Int)Int
scala> double(3)
res0: Int = 6
scala> double(1)
res1: Int = 2

Une fonction peut avoir plusieurs arguments par exemple la fonction faisant la somme de deux nombres entiers prend deux arguments. Soient x et y les deux entiers dont nous souhaitons faire la somme, la fonction add est définie comme suit :

Cette définition se traduit aisément en Scala de la façon suivante :
def add(x:Int, y:Int) = x + y
Le test de la fonction donne :
$ scala
scala> def add(x:Int, y:Int) = x + y
add: (x: Int, y: Int)Int
scala> add(1, 2)
res0: Int = 3

Et si nous souhaitions définir une fonction qui incrémente un entier nous pouvons réutiliser la fonction add. En effet incrémenter un entier revient à lui ajouter 1 :

def increment(number: Int) = add(number, 1)
En action cela donne:
$ scala
scala> def increment(number: Int) = add(number, 1)
increment: (number: Int)Int
scala> increment(3)
res1: Int = 4
scala> increment(10)
res2: Int = 11

Note : Une propriété importante de chacune des fonctions que nous avons vues jusque là est le fait que la valeur de retour est entièrement déterminée par les arguments de la fonction.

Munis de ces informations passions aux fonctions d’ordre supérieur.

Les fonctions d’ordre supérieur

Une fonction d’ordre supérieur est une fonction qui a au moins l’une des deux caractéristiques suivantes :

  • prend en paramètre une ou plusieurs fonctions
  • sa valeur de retour est une fonction

Les fonctions d’ordre supérieur aident à l’expressivité du code en mettant l’attention sur la tâche à accomplir plutôt que sur la façon de l’accomplir : le code dévient ainsi plus déclaratif. Cela devient plus clair avec un exemple. Considérons la classe Person ci-dessous :

public class Person {
 private final String name;
 private final String email;
 private final int age;

 public Person(String name, String email, int age) {
  this.name = name;
  this.email = email;
  this.age = age;
 }

 public String getName() {
  return name;
 }

 public String getEmail() {
  return email;
 }

 public int getAge() {
  return age;
 }
}
Supposons que vous disposez d’une liste d’objets de type Person sur laquelle vous souhaitez faire différentes opérations : transformation, filtrage etc. Commençons par la transformation de la liste.

Transformation des éléments d’une collection

Voici la liste initiale :

List<Person> persons = Arrays.asList(//
    new Person("Toto", "toto@email.com", 23), //
    new Person("John Doe", "john.doe@email.com", 34), //
    new Person("Mad Max", "mad.max@crazymail.com", 20), //
    new Person("Jane Doe", "jane@email.com", 17));
La première transformation qui nous intéresse est l’extraction des adresses email à partir de la liste des personnes. C’est ce que fait la méthode extractEmails :
public static List<String> extractEmails(List<Person> persons) {
 List<String> emails = new ArrayList<String>();

 for (Person person : persons) {
  emails.add(person.getEmail());
 }

 return emails;
}
Testons ce que cela donne :
System.out.println("Emails = " + extractEmails(persons));
Sortie de la console :
Emails = [toto@email.com, john.doe@email.com, mad.max@crazymail.com, jane@email.com]
On obtient une liste ayant la même taille que la liste des personnes mais ne contenant que les adresses email. Le code de la méthode extractEmails recèle quelques défauts dont la “dilution” de l’intention de la méthode dans du code verbeux.

Avant de voir comment résoudre ces défauts implémentons une autre fonctionnalité : extraire les noms à partir de la liste des personnes :

public static List<String> extractNames(List<Person> persons) {
 List<String> names = new ArrayList<String>();

 for (Person person : persons) {
  names.add(person.getName());
 }

 return names;
}

Cette méthode fait ressortir un autre défaut de notre code : la duplication. Les deux méthodes ne diffèrent véritablement que par la transformation effectuée à chaque itération sur l’élément de la liste initiale. Cette transformation peut être matérialisée par une fonction f qui prend comme argument un objet de type Person et retourne son nom ou son adresse email. Ainsi l’implémentation de l’une ou l’autre de nos fonctions se résume comme suit : "applique la fonction f à chaque élément de la liste initiale et renvoie-moi la liste correspondante."

Nous pouvons schématiser cette phrase de la façon suivante, avec à gauche la liste initiale et à droite la liste obtenue suite à la transformation :

Cela correspond exactement à ce que fait la fonction map. La figure ci-dessous donne le fonctionnement de la méthode map présente sur la classe List de Scala.

Comme mentionné sur la figure la fonction map prend en paramètre une fonction, celle que nous souhaitons appliquer à chaque élément de la liste initiale. Dans notre cas la fonction a comme type Person => String. Nous allons maintenant réimplémenter nos deux méthodes en utilisant la méthode map disponible dans l'API des listes Scala.

Les fonctions extractEmails et extractNames revisitées

Définition de la classe Person :

case class Person(name: String, email: String, age: Int)
Nous définissons d’abord la fonction qui extrait une adresse email à partir d’une instance de Person :
     
def extractEmail(person: Person) = person.email
Puis nous passons cette fonction à la méthode map de la liste afin de récupérer la liste des adresses email.
def extractEmails(persons: List[Person]): List[String] = persons.map(extractEmail)
Sur le même principe voici la définition de la fonction extractNames :
     
def extractName(person: Person) = person.name
def extractNames(persons: List[Person]): List[String] = persons.map(extractName)
Note : Grâce aux fonctions anonymes nous pouvons nous passer des fonctions extractEmail et extractName qui n’ont pas grand intérêt.
   
def extractNames(persons: List[Person]): List[String] = persons.map(p => p.name)
def extractEmails(persons: List[Person]): List[String] = persons.map(p => p.email)

Qu'avons-nous gagné par rapport à avant ? Le code est plus expressif en éliminant le bruit et en mettant l’accent sur l’intention du code : "mapper chaque élément de la liste sur son application à la fonction que nous passons à la méthode map". Les fonctions d’ordre supérieur rendent pratique une autre opération courante sur les collections : le filtrage.

Filtrer une collection

Nous disposons d’une liste de nombres entiers allant de 1 à 15 et nous souhaitons filtrer cette liste en éliminant tous les éléments qui ne sont pas des multiples de 3. Ce genre d’opération est résolu en programmation fonctionnelle avec la fonction d’ordre supérieur filter. Elle prend en arguments une liste et un prédicat, une fonction qui retourne "vrai" ou "faux". Dans notre exemple le prédicat retourne “vrai” si son argument est un multiple de 3 et "faux" dans le cas contraire. Scala étant un langage orienté-objet filter est une méthode de List. Voici la définition du prédicat :

scala> def isMultipleOfThree(number: Int) = number % 3 == 0
isMultipleOfThree: (number: Int)Boolean
scala> isMultipleOfThree(2)
res0: Boolean = false
scala> isMultipleOfThree(6)
res1: Boolean = true
Et la liste de nombres :
scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
numbers: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
Le filtrage s’effectue simplement en passant la fonction isMultipleOfThree à la méthode filter de la liste de nombres :
scala> val multiplesOfThree = numbers.filter(isMultipleOfThree)
multiplesOfThree: List[Int] = List(3, 6, 9, 12, 15)
Ceci peut être réécrit en remplaçant isMultipleOfThree par une fonction anonyme :
scala> numbers.filter(number => number % 3 == 0)
res2: List[Int] = List(3, 6, 9, 12, 15)
Nous allons maintenant combiner filter et map.

Combiner filter et map

Nous souhaitons obtenir la somme des doubles des multiples de 3 compris entre 1 et 15. Cela correspond à une opération de filtrage (suppression des nombres non multiples de 3) suivie d’une transformation des éléments (doublement des éléments des éléments restants) et enfin une opération de réduction (la sommation). On y arrive aisément avec le code suivant :

scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
numbers: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
scala> numbers.filter(number => number % 3 == 0).map(number => number * 2).sum
res7: Int = 90
Il nous reste une dernière fonction d’ordre supérieur à découvrir avant de conclure cet article : la fonction fold.

La fonction fold, le couteau suisse

La fonction fold est un véritable couteau suisse. Elle réduit les éléments d’une liste en une valeur. Cette fonction existe en deux versions : l’une (foldLeft) parcourt la liste de la gauche vers la droite et l’autre de la droite vers la gauche (foldRight). Nous allons plutôt utiliser la première dont voici la signature :

def foldLeft[B](z: B)(f: (B, A) ⇒ B): B
C’est une fonction curryfiée. Elle prend une valeur initiale de type B, désignée par z et une fonction f prenant deux arguments de types A (le type des éléments de la liste) et B. Pour chaque élément de la liste la fonction f est invoquée. f est invoquée avec la valeur initiale z fournie à la méthode foldLeft. Puis la valeur résultante est passée avec le deuxième élément de la liste à la fonction f. Ce processus continue avec tous les éléments de la liste. Pensez à lire cet article si vous souhaitez en savoir davantage. Place à l’action ! Afin de comprendre comment fonctionne foldLeft nous allons résoudre trois petits problèmes. Le premier problème consiste à faire la somme des éléments d’une liste d’entiers. La valeur initiale dans le cas de la somme est 0 (l’élément neutre de l’addition) et la fonction f additionne ces deux paramètres. Voici ce que cela donne dans le REPL :
scala> val nums = List(1, 2, 3, 4)
nums: List[Int] = List(1, 2, 3, 4)
scala> val sum = nums.foldLeft(0)((acc, num) => acc + num)
sum: Int = 10     
scala> val nums2 = List(1, 3, 5, 7)
nums2: List[Int] = List(1, 3, 5, 7)
scala> val sum2 = nums2.foldLeft(0)((acc, num) => acc + num)
sum2: Int = 16
Simple non ?! Ne nous arrêtons pas en si bon chemin continuons sur le deuxième problème : faire le produit des éléments d'une liste. La valeur initiale passée à foldLeft dans ce cas est l’élément neutre de la multiplication à savoir 1. Quant à la fonction à passer foldLeft elle fait le produit de ces deux paramètres.
     
scala> val nums = List(1, 2, 3, 4)
nums: List[Int] = List(1, 2, 3, 4)
scala> val prod = nums.foldLeft(1)((acc, num) => acc * num)
prod: Int = 24
scala> val nums2 = List(1, 3, 5, 7)
nums2: List[Int] = List(1, 3, 5, 7)
scala> val prod2 = nums2.foldLeft(1)((acc, num) => acc * num)
prod2: Int = 105
So far so good! Maintenant résolvons le dernier exercice pour la route. Il s’agit de construire à partir d’une liste d’entiers une map dont les clés sont les éléments pairs de la liste et les valeurs leurs doubles. Voyons un exemple afin de clarifier les choses :
List(7, 2, 10, 3, 6) -> Map(2 -> 4, 10 -> 20, 6 -> 12)
La valeur initiale est une map vide. La fonction passée à foldLeft prend une map et un entier. Si l’entier est pair son double est mappé sur sa valeur. La fonction retourne la map résultante.
    
scala> val nums = List(1, 2, 3, 4)
nums: List[Int] = List(1, 2, 3, 4)

scala> val evenDoubled = nums.foldLeft(Map[Int, Int]()) { (acc, num) =>
| if (num % 2 == 0) acc + ((num, 2 * num)) else acc
| }
evenDoubled: scala.collection.immutable.Map[Int,Int] = Map(2 -> 4, 4 -> 8)

Pour conclure

Nous arrivons à la fin de cette introduction aux fonctions d’ordre supérieur. Elles sont particulièrement adaptées aux traitements sur des collections. Mais leur usage va au-delà des collections. Consultez la liste de références ci-dessous pour aller loin.

Références

[Intro à Scala 2] Premiers pas avec Scala

Après une introduction générale à Scala et à son écosystème dans le premier article, nous allons installer Scala puis écrire nos premières lignes de code Scala afin de nous familiariser avec le REPL.

Télécharger Scala

Pour fonctionner Scala a besoin de Java. Vous devez donc vous assurer que Java est installé sur votre machine avant de procéder à l’installation de Scala. Dans cette série d’articles nous utiliserons la version 2.10.2 de Scala et cela requiert au moins la version 1.6 de Java. Commencez d’abord par vérifier la disponible de Java sur votre système avec la commande suivante :

$ java -version

En exécutant cette commande sur ma machine j’obtiens la sortie suivante :

$ java -version
java version "1.6.0_51"
Java(TM) SE Runtime Environment (build 1.6.0_51-b11-457-11M4509)
Java HotSpot(TM) 64-Bit Server VM (build 20.51-b01-457, mixed mode)

Vous devez avoir une sortie similaire si Java est installé sur votre machine sinon rendez-vous à l’adresse http://www.java.com pour le télécharger.

Maintenant que vous avez installé Java procédons à l’installation de Scala. En fonction de votre plate-forme elle ne se fera pas de la même manière.

Windows

Allez à l’adresse http://www.scala-lang.org/download/2.10.2.html et cliquez sur le lien nommé scala-2.10.2.msi. Cela va télécharger un fichier qui permet d’installer Scala sur votre machine Windows. Une fois le téléchargement terminé il suffit de cliquer dessus et de suivre les différentes étapes de l’installation. Une fois l’installation terminée vous pouvez vérifier si cela a marché avec la commande :

$ scala -version

Vous obtiendrez une sortie similaire à :

$ scala -version
Scala code runner version 2.10.2 -- Copyright 2002-2013, LAMP/EPFL

Unix/Linux, Mac OS X et Cygwin

Téléchargez le fichier archive disponible à l’adresse http://www.scala-lang.org/files/archive/scala-2.10.2.tgz puis extrayez-le à l’endroit de votre choix. Sur ma machine je vais l’installer dans /usr/local/share.

$ cd /usr/local/share/
$ sudo wget http://www.scala-lang.org/files/archive/scala-2.10.2.tgz

Une fois le téléchargement terminé j’extrais les fichiers :

$ tar xvfz scala-2.10.2.tgz

Histoire de ne pas lier mon installation à une version particulière je crée un lien symbolique vers le dossier scala-2.10.2.

$ sudo ln -s /usr/local/share/scala-2.10.2 /usr/local/share/scala

Ensuite éditez par exemple votre fichier .bashrc pour lui ajouter les lignes suivantes :

SCALA_HOME=/usr/local/share/scala
export PATH=$PATH:$SCALA_HOME/bin

Enfin vous pouvez vérifier l’installation comme suit :

$ source ~/.bashrc
$ scala -version
Scala code runner version 2.10.2 -- Copyright 2002-2013, LAMP/EPFL

Que contient l’installation de Scala ?

Faisons le tour du propriétaire pour comprendre ce que contient notre installation de Scala. Pour cela je me place dans le dossier où est installé Scala puis je lance la commande tree :

$ cd /usr/local/share/scala
$ tree -d -L 2

Cela me donne la structure suivante :

$ tree -d -L 2
.
├── bin
├── doc
│   └── tools
├── examples
│   ├── actors
│   ├── monads
│   ├── parsing
│   ├── tcpoly
│   └── xml
├── lib
├── man
│   └── man1
├── misc
│   └── scala-devel
└── src

15 directories

Voici une brève description des dossiers ci-dessus :

  • bin : Ce dossier contient les programmes exécutables comme le compilateur de Scala scalac, le dé-compilateur scalap ou l'interpréteur scala. Lors de l’installation ce dossier a été ajouté au chemin où le système d’exploitation cherche les commandes que nous tapons dans le terminal. Ainsi nous n’avons pas besoin de taper le chemin complet des exécutables.
  • doc : Ce dossier contient de la documentation sur les commandes localisées dans le repertoire bin.
  • examples : Ici vous trouverez des exemples de programmes écrits en Scala. Faites-y un tour afin de voir à quoi ressemble la syntaxe de Scala ou pour trouver de l’inspiration.
  • lib : C’est là que résident les librairies c’est-à-dire un ensemble de fonctions et de classes prêtes à l’emploi que vous pouvez utiliser pour écrire vos propres programmes.
  • man : Le contenu de ce dossier est utilisé par la commande man qui affiche l’aide sur des commandes comme scala ou scalac. Essayez ça :
    $ man scala
    
  • src : Le code source de Scala et des librairies se trouvent ici.
  • misc : Ce dossier est, j’ai envie de dire, sans grand intérêt pour nous. A ce jour il contient le plugin pour la contituation.

Premiers pas avec l’interpréteur de Scala ou le REPL

Comme vous allez vous en rendre l’interpréteur de Scala est un outil puissant pour expérimenter des choses et accélérer son apprentissage du langage. Vous avez un doute et hop lancez l’interpréteur pour le lever ! Vous avez une idée et hop l’interpréteur est là pour la tester.

Et pour lancer l’interpréteur rien de plus simple que de taper la commande scala dans une console :

$ scala
Welcome to Scala version 2.10.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_51).
Type in expressions to have them evaluated.
Type :help for more information.
scala>

Vous voilà dans l’interpréteur prêt à taper des commandes. Tapez un nombre et appuyez sur la touche Enter du clavier pour voir.

scala> 20
res0: Int = 20

Le nombre 20 sur la première ligne correspond à ce que j’ai tapé et la deuxième ligne à ce que me répond l’interpréteur. Que s’est-il passé ? L’interpréteur a récupéré ce que j’ai tapé (ici 20), l’a évalué puis a affiché le résultat en lui donnant le nom res0 (pour résultat numéro 0) et la valeur 20. Désormais res0 contient le nombre 20. Pour vous en convraincre tapez res0 dans l’interpréteur :

scala> res0
res1: Int = 20

Note : Notez que le nom res0 est suivi de “: Int”. Cette annotation nous précise que le nom res0 contient un entier. Nous reviendrons sur ce point dans la suite.

Recommençons avec une opération un peu plus compliquée : l’addition de deux nombres, par exemple 2 + 7.

scala> 2 + 7
res2: Int = 9

Là encore le REPL a évalué ce que nous avons tapé puis a affiché le résultat.

Il est aussi possible de réutiliser le résultat d’une opération précédente :

scala> res2 + 2
res3: Int = 11
scala> res3 - 8
res4: Int = 3

En dehors du code Scala l’interpréteur de Scala accepte aussi d’autres commandes dont la plus importante est :help. Elle vous listera la liste des commandes disponibles. Pour l’instant je mets l’accent sur trois commandes seulement dont voici la description :

  • :cp : Cette commande vous permet d’utiliser depuis le REPL du code situé dans un fichier archive Java (avec l’extension .jar) ou dans un répertoire.
  • :quit : Celle-là est utilisée pour quitter le REPL. Eh oui il faut pouvoir en sortir !
  • :paste : Cette commande s’avère assez pratique quand on écrit du code sur plusieurs lignes dans le REPL.

Même si le REPL est symphatique comme calculatrice elle peut faire bien plus comme afficher un message sur l’écran par exemple.

scala> println("Bonjour ! Ça se passe bien jusque là ?")
Bonjour ! Ça se passe bien jusque là ?

Essayez la même chose chez vous, n’hésitez pas à changer le message. Ici nous avons utilisé la fonction println pour afficher notre message à l’écran. Nous verrons dans la suite ce qu’est une fonction mais si vous êtes pressé de le savoir lisez cet article.

Résumé et programme pour la suite

Mine de rien nous avons déjà fait beaucoup de choses. Nous avons installé Scala, appris à lancer le REPL et à le faire exécuter du code Scala pour nous. Dans le prochain article nous nous baserons sur ces connaissances pour apprendre de nouveaux concepts et nous initier à la syntaxe de Scala.

Introduction à Scala - Episode 1

Scala est un langage qui a du caractère. Il fait réagir du monde; ceux qui l’ont essayé et l’ adorent, ceux qui ne l’ont jamais essayé mais le détestent et les autres. Parmi ce beau monde les plus étonnants sont ceux qui veulent accuser Scala des travers des langages (comme le C++) qu’ils ont aimé détester. Au lieu de vous laisser influencer par les autres essayez par vous-même ce langage qui a fait le pari audacieux de marier les paradigmes fonctionnel et orienté objet.

A propos de cette série d’articles

Je souhaite à travers une série d’articles introduire le langage de programmation Scala. Cela a un double intérêt pour moi. D’une part ce sera l’occasion pour moi d’approfondir ma connaissance du langage et d’autre part j'accompagne les premiers pas de ceux qui souhaitent l’apprendre.

D’où vient Scala et que propose-t-il ?

Scala est un langage de programmation créé par Martin Ordersky professeur à l’École Polytechnique Fédérale de Lausane EPFL en Suisse. Sa première version est rendue publique en 2003. Son nom vient de l’abréviation de “scalable language” signifiant qu’il est capable de s’adapter aux contraintes de passage à l’échelle (scalabilité) du développeur en lui fournissant des “abstractions” et des constructions à cet effet. La particularité de Scala est qu’il intègre à la fois les paradigmes de programmation fonctionnel et orienté objet. Il est statiquement typé et s’exécute sur la machine virtuelle Java (JVM).

Dans la suite de la série je reviendrai plus en détail sur ces deux paradigmes. Le point clé de ce mariage entre les paradigmes orienté objet et fonctionnel est qu’il vous offre plus d’options pour résoudre vos problèmes. Les différentes possibilités d’extension du langage facilitent l’expression de logiques métier complexes et de produire du code qui s’intègrent d’une manière élégante au reste du langage. La bibliothèque standard de Scala est un bon exemple de cette puissance expressive.

Un autre aspect de Scala est qu’il dispose d’une inférence de type qui allège le code et en réduit fortement la verbosité. C’est agréable et on a parfois l’impression de travailler avec un langage dynamique.

Scala vient avec un interpréteur qui porte le doux nom de REPL pour Read-Eval-Print-Loop. Comme vous l’avez peut-être deviné le REPL vous laisse taper du code dans une console, l’évalue, affiche le résultat et attend de nouveau que vous tapiez quelque chose. Vous pouvez vous servir du REPL pour tester rapidement vos idées et faire du prototypage. Il est idéal pour apprendre le langage et nous ne manquerons pas de nous en servir pour cela.

Quel écosystème pour Scala ?

La plate-forme Java est aujourd’hui très mature et bien introduite en entreprise. Les outils et les librairies sont nombreuses et matures. En outre les développeurs Java se comptent en milliers. Scala tire pleinement partie de cette maturité et de la puissance qu’elle confère à Java en s’intégrant facilement avec le langage Java. Un programme écrit en Scala peut accéder à n’importe quel code écrit en Java et tourne, après compilation, sur la machine virtuelle Java.

Au-delà de la force que lui confère Java, l’écosystème de Scala compte de nombreux projets et outils emblématiques dont voici quelques uns :

  • Akka : Akka propose un ensemble d’outils pour créer des applications concurrentes et distribuées. Il utilise les acteurs comme modèle de programmation.
  • Play : Un framework pour la création d’applications web
  • Scalding : Une librairie Scala qui rend facile la spécification des tâches Hadoop.
  • Specs2 et ScalaTest : Des librairies de test populaires écrits en Scala.
  • Spray : Basé sur Akka, Spray propose des interfaces de programmation (API) pour écrire des applications HTTP performantes

Les outils, les outils !

Commençons par les outils de build. Un projet Scala peut être buildé par les outils de build majeurs en Java : Maven, Gradle et Apache buildr. En outre il existe un autre outil, SBT (Simple Build Tool), écrit en Scala pour builder des projets Scala et Java. SBT est utilisé par des projets comme Akka, Play, Spray etc.

Eclipse, Netbeans et Intellij ont des plugins pour l’intégration Scala. Il est aussi possible d’utiliser d’autres environnement comme Sublime Text, emacs, vim etc.

Autres ressources pour apprendre à programmer en Scala

Il existe d’autres ressources disponibles gratuitement sur Internet pour apprendre à programmer en Scala. Voici une liste non exhaustive de certaines de ces ressources :

  • http://docs.scala-lang.org : Un site plutôt bien fait et assez complet pour apprendre le langage. Vous y trouvez une quantité incroyable d’information.
  • La première édition du livre écrit par Odersky, créateur de Scala, est en accès libre en ligne à cette adresse : http://www.artima.com/pins1ed/.
  • Scala School par Twitter : Twitter a publié une introduction rapide à Scala. Vous pouvez y jeter un coup d'oeil en allant à l’adresse http://twitter.github.io/scala_school/.
  • Martin Odersky donne un cours d’introduction à la programmation fonctionnelle à travers Scala sur https://www.coursera.org. Environ 50 000 personnes se sont inscrites à la première édition du cours. Faites sur un tour sur coursera afin de vous renseigner sur les éditions à venir.

J’ai besoin de vous !

N’hésitez pas à réagir à ce que j’écris ici ou à me corriger à travers les commentaires et à me faire des suggestions sur le contenu et la forme des articles.

C’est quoi la suite ?

Dans le prochain nous allons installer Scala et faire nos premiers avec le REPL. Ce sera pour vous l’occasion d’écrire du code Scala.

Le deuxième article a été publié.

Installer Scala sous Linux

Dans ce court post je partage comment j'installe Scala sur mon PC tournant sous Ubuntu. Commencez d'abord par télécharger depuis le site web de Scala le fichier archive (au format .tgz) de la dernière version du langage : http://www.scala-lang.org/downloads. La version actuelle de Scala est la 2.10 et c’est celle que nous allons installer.

Voici comment l'obtenir via la ligne de commande :

$ wget http://www.scala-lang.org/downloads/distrib/files/scala-2.10.0.tgz
Ensuite nous extrayons le contenu du fichier archive :
$ tar xvfz scala-2.10.0.tgz
Les fichiers sont extraits dans le dossier scala-2.10.0. La sortie suivante de la commande tree montre la structure des fichiers.
$ cd scala-2.10.0
$ tree -L 1
.
├── bin
├── doc
├── examples
├── lib
├── man
├── misc
└── src

Les exécutables se trouvent dans le dossier bin et les librairies dans le dossier lib. Sur mon ordinateur les fichiers se trouvent dans ~/tools/scala-2.10.0 sur lequel j’ai créé le lien symbolique /opt/scala puisque je souhaite pouvoir changer facilement de version de Scala.

$ sudo ln -s ~/tools/scala/scala-2.10.0 /opt/scala

Vérifions l’installation en affichant la version de Scala :

$ /opt/scala/bin/scala -version
Scala code runner version 2.10.0 -- Copyright 2002-2012, LAMP/EPFL
Ça marche mais ce n’est pas pratique de taper le chemin complet de la commande à chaque lancement de l’interpréteur Scala. Afin de corriger cela j’ajoute le dossier /opt/scala/bin au chemin du système avec la commande suivante :
$ export PATH=$PATH:/opt/scala/bin
En mettant la ligne précédente dans le fichier .bashrc les exécutables se trouvant dans /opt/scala/bin seront ajoutés au chemin du système à l’ouverture d’une session.
$ scala
Welcome to Scala version 2.10.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_26).
Type in expressions to have them evaluated.
Type :help for more information.
scala> println ("Now go play with Scala !")
Now go play with Scala !

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 !

Subdivisez vos collections avec groupBy

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)))))

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.

Listes Scala: méthodes foldLeft et foldRight

Dans ce billet je souhaite vous parler rapidement des méthodes foldLeft et foldRight de l’API des listes en Scala.

foldLeft


foldLeft est une méthode de la classe scala.collection.immutable.List et voici ce que le scaladoc nous en dit:
“Applies a binary operator to a start value and all elements of this list, going left to right.”
Scaladoc nous dit que foldLeft applique une fonction prenant deux paramètres (opérateur binaire) à une valeur initiale (appelée accumulateur dans le jargon fonctionnel) et à tous les éléments de la liste en partant de la gauche. Voyons attentivement la signature de la méthode pour rendre les choses un peu plus claires!

def foldLeft [B] (z: B)(f: (B, A) ⇒ B): B

Ah tiens une chose transparaît dans cette signature: foldLeft est une méthode currifiée. Si vous n’avez jamais fait de programmation fonctionnelle auparavant cette notion ne doit pas vous parler. En fait malgré les apparences la méthode foldLeft ne prend pas deux arguments! Voici comment elle fonctionne: elle prend un paramètre z de type B et renvoie une fonction qui prend à son tour un paramètre qui est une fonction de type (B, A) => B. z est représente la valeur initiale de l’accumulateur et est du même type que la valeur de retour de foldLeft. A chaque étape la fonction f est appliquée à l’accumulateur et à l’élément courant de la liste. La valeur de l’accumulateur peut changer tout au long du déroulement de l’opération. Prenez une minute pour bien visualiser la chose ce n’est pas si compliqué que cela une fois qu’on a compris la notion. La méthode fold est d’une puissance incroyable! Elle vous permet de faire quasiment de faire toutes les opérations imaginables avec les listes.

Voici un usage simple de foldLeft: Calculer la somme des éléments d’une liste d’entiers.

L’accumulateur (la valeur initiale) est égal à zéro et l’opérateur binaire est une fonction qui prend deux entiers et fait leur somme:

val nums = List(1, 2, 3, 4)
val sum = nums.foldLeft(0){
 (acc, num) => acc + num
}

En action dans le REPL:
scala> val nums = List(1, 2, 3, 4)
nums: List[Int] = List(1, 2, 3, 4)

scala> val sum = nums.foldLeft(0){
 (acc, num) => acc + num
}
sum: Int = 10

Le déroulé de l’opération foldLeft est le suivant :
((((0 + 1) + 2) + 3) + 4)
((((1) + 2) + 3) + 4)
(((3) + 3) + 4)
((6) + 4)
(10)

En image cela donne:


foldRight


foldRight est assez similaire à foldLeft mais il parcourt la liste en partant de la droite de la liste. Ainsi c’est le dernier élément de la liste qui est d’abord traitée. Voyons l’implémentation de la somme des éléments d’une liste avec foldRight:

scala> val nums = List(1, 2, 3, 4)
nums: List[Int] = List(1, 2, 3, 4)
scala> val sum = nums.foldRight(0) {(num, acc) =>
 num + acc
}
sum: Int = 10
Le déroulé de l’opération foldLeft est le suivant :

(1 + (2 + (3 + (4 + 0))))
(1 + (2 + (3 + (4))))
(1 + (2 + (7)))
(1 + (9))
(10)


Conclusion


Prenez le temps de digérer ce contenu si jamais l’envie me prend dans l’avenir je reviendrais sur une comparaison plus avancée entre ces deux méthodes.

S-99: P06, P07

Je continue la résolution des 99 problèmes en Scala. Au fur et à mesure que j'avance dans la résolutions de ces problèmes je me rends compte je fais beaucoup usage du pattern matching. Et cet épisode ne fait pas exception à la règle!


Le problème P06: Vérifier si une liste est un palindrome

C'est par là si vous voulez en savoir davantage sur le palindrome.
Exemple

scala> isPalindrome(List(1, 2, 3, 2, 1))
res0: Boolean = true


La solution à P06

Je suis rapidement arrivé à la solution suivante consistant à comparer la liste avec son inverse. J'ai ensuite cherché d'autres plus "efficace" mais je ne suis pas arrivé à trouver une solution plus "fonctionnelle" à mon goût.

def isPalindrome[T](xs: List[T]) = xs.reverse == xs


Le test de la solution

import org.specs2.mutable._

class P06Spec extends Specification {
  "P06.isPalindrome(xs)" should {    
    "return true" in {
      P06.isPalindrome(List()) must_== true
    }
    
    "return true" in {
      P06.isPalindrome(List(1)) must_== true
    }
    
    "return true" in {
      P06.isPalindrome(List(60, 60)) must_== true
    }
    
    "return true" in {
      P06.isPalindrome(List(3, 2, 1, 2, 3)) must_== true
    }
  }
}


Le problème P07: Mettre à plat une liste de listes

Il s'agit de mettre à plat une liste dont les constituants peuvent être des listes à leur tour. C'est pour le fun que nous écrivons cette fonction mais pas une nécessité car l'API des listes fournit une méthode permettant de mettre à plat une liste.
Exemple:

scala> flatten(List(List(1, 1), 2, List(3, List(5, 8))))
res0: List[Any] = List(1, 1, 2, 3, 5, 8)


La solution à P07

La solution utilise le pattern matching et la récursivité à fond!

  def flatten(xs: List[Any]): List[Any] = {
    xs match {
      case head :: tail => (head, tail) match {
        case (h: List[Any], t) => flatten(h) ++ flatten(t)
        case (h: Any, t) => h :: flatten(t)
      }
      case x => x
    }
  }


Le test de la solution

import org.specs2.mutable._

object P07Spec extends Specification {
  "P07.flatten(xs)" should {
    "return Nil" in {
      P07.flatten(Nil) must_== Nil
    }
    
    "return List(1)" in {
      P07.flatten(List(1)) must_== List(1)
    }
    
    "return List(1, 2)" in {
      P07.flatten(List(1, List(2))) must_== List(1, 2)
    }
    
    "return List(1, 2, 3, 4)" in {
      P07.flatten(List(1, List(2, 3), 4)) must_== List(1, 2, 3, 4)
    }
    
    "return List(1, 2, 3, 4, 5, 7, 8)" in {
      P07.flatten(List(1, List(2, 3), 4, List(5, List(6, List(7, 8))))) must_== List(1, 2, 3, 4, 5, 6, 7, 8)
    }
  }
}


Fin du voyage mes amis! A bientôt pour un autre épisode ;-)

S-99 : P02, P03, P04, P05

Avant d'aller plus loin laissez-moi vous dire que d'intéressantes discussions sont en cours sur la mailing liste du PSUG (Paris Scala User group). Je vous encourage à vous inscrire afin de prendre part aux échanges.
Nos oignons maintenant!

Le problème P02

Il s'agit en fait d'écrire une fonction qui retourne l'avant dernier élément d'une liste.
Voici l'exemple donné sur le site du S-99.

scala> penultimate(List(1, 1, 2, 3, 5, 8))
res0: Int = 5


La solution au P02

Voici une des solutions auxquelles je suis arrivé:

def penultimate[T](xs: List[T]) = {
      xs.reverse match {
        case _ :: x :: _ => Some(x)
        case _ => None
      }
    }


Le test de la solution à P02

import org.specs2.mutable._

class P02Spec extends Specification {
  "P02.penultimate(List())" should {
    
    "return None" in {
      P02.penultimate(List()) must_== None
    }

    "return None" in {
      P02.penultimate(List(1)) must_== None
    }

    "return Some(2.5)" in {
      P02.penultimate(List(3.23, 2.4)) must_== Some(3.23)
    }

    "return Some(2)" in {
      P02.penultimate(List(1, 2, 3)) must_== Some(2)
    }

    "return Some(3)" in {
      P02.penultimate(List(1, 2, 3, 4)) must_== Some(3)
    }
  }
}

Le problème P03

Il s'agit d'écrire une fonction qui renvoie l'élément correspondant à un index donné.
Exemple:

scala> nth(2, List(1, 1, 2, 3, 5, 8))
res0: Int = 2


La solution à P03

Avant d'aller plus loin permettez moi de vous dire que cet exercice n'a rien de nécessaire car l'API des listes permet de récupérer l'élément à un index donné de la façon suivante:

val element = xs(index)
Maintenant l'une des solutions que j'ai pu avoir:
def nth[T](index: Int, xs: List[T]):Option[T] = {
    (index, xs) match {  
      case (_, Nil) => None
      case (0, head :: tail) => Some(head)
      case (_, head :: tail) if(index == -1 || index > (xs.length + 1)) => None
      case (_, head :: tail) => nth(index - 1, tail)
    }
  }
Remarquez l'usage du pattern matching sur le tuple formé de l'index et de la liste.

Le test de la solution à P03

Enfin le test:

import org.specs2.mutable._

class P03Spec extends Specification {
  "P03.nth(index, xs)" should {
    
    "return None" in {
      P03.nth(-1, List(1, 3)) must_== None
    }

    "return Some(1)" in {
      P03.nth2(0, List(1, 3)) must_== Some(1)
    }

    "return Some(4)" in {
      P03.nth(2, List(2, 3, 4)) must_== Some(4)
    }

    "return None" in {
      P03.nth2(10, List(2, 3, 4, 5)) must_== None
    }

    "return None" in {
      P03.nth(0, List()) must_== None
    }    
  }
}

Le problème P04

Il s'agit encore une fois d'implémenter une fonctionnalité déjà présente dans l'API des listes mais c'est pour le fun: trouver le nombre d'éléments dans une liste
Exemple:

scala> length(List(1, 1, 2, 3, 5, 8))
res0: Int = 6

La solution au P04

Voici deux solutions à ce problème: l'une utilise la recursivité et l'autre la méthode foldLeft() de l'API des listes.

def lengthRecursive[T](xs: List[T]): Int = {
    xs match {
      case Nil => 0
      case head :: tail => 1 + lengthRecursive(tail)
    }
  }
  
def length[T](xs: List[T]) = {
  xs.foldLeft(0)((x, _) => x + 1)
}


Le test à la solution au P04

import org.specs2.mutable._

class P04Spec extends Specification {
  "P04.length(xs)" should {
    
    "return 0" in {
      P04.length(List()) must_== 0
    }

    "return 4" in {
      P04.length(List(1, 2, 3, 4)) must_== 4
    }

    "return 5" in {
      P04.length(List(1, 2, 3, 4, 5)) must_== 5
    }

    "return 7" in {
      P04.length(List(1, 2, 3, 4, 5, 6, 7)) must_== 7
    }
  }
}


Le problème P05

Il s'agit d'écrire une liste qui inverse une liste (pour info cette méthode existe déjà dans l'API des listes).
Exemple:

scala> reverse(List(1, 1, 2, 3, 5, 8))
res0: List[Int] = List(8, 5, 3, 2, 1, 1)


La solution au P05

def reverse[T](xs: List[T]):List[T] = xs match {
    case Nil => Nil
    case head :: tail => reverse(tail) :+ head
  }
  


Le problème P05

Enfin un petit test pour s'assurer que tout se passe bien!

import org.specs2.mutable._

class P05Spec extends Specification {
  "P05.reverse(xs)" should {
    
    "return Nil" in {
      P05.reverse(List()) must_== Nil
    }

    "return List(4, 3, 2, 1)" in {
      P05.reverse(List(1, 2, 3, 4)) must_== List(4, 3, 2, 1)
    }

    "return List(5, 4, 3, 2, 1)" in {
      P05.reverse(List(1, 2, 3, 4, 5)) must_== List(5, 4, 3, 2, 1)
    }

    "return List(7, 6, 5, 4, 3, 2, 1)" in {
      P05.reverse(List(1, 2, 3, 4, 5, 6, 7)) must_== List(7, 6, 5, 4, 3, 2, 1)
    }
  }
}


Conclusion

J'espère que les quelques solutions vous ont donné envie de suivre les prochains épisodes du S-99. Ne décrochez pas là les problèmes deviennent de plus en plus intéressants!
A bientôt!

S-99 : P01

J'ai entrepris de résoudre les problèmes du "S-99: Ninety-Nine Scala Problems", histoire de faire du Scala et peut-être de confronter mes solutions avec celles de mes lecteurs. Donc n'hésitez pas à poster vos solutions en commentaires! J'espère qu'à la fin de cette série de problèmes mon cerveau pensera mieux Scala ;-)


Le problème

Il s'agit d'écrire une fonction qui retourne le dernier élément d'une liste d'entiers.
Exemple fourni par le site:

scala> last(List(1, 1, 2, 3, 5, 8))
res0: Int = 8


Les solutions

Voici ma première implémentation:

def last1[T](xs: List[T]) = xs match {
    case List() => None
    case _ => Some(xs(xs.size - 1))
  }
Vous aurez remarqué que je ne renvoie pas directement le résultat mais plutôt un objet Option.
Voici une deuxième implémentation, plus courte, qui utilise la méthode headOption qui renvoit une Option avec sur l'élément de tête d'une liste. Remarquez que j'inverse d'abord la liste.
def last2[T](xs: List[T]): Option[T] = xs.reverse headOption
Enfin la version récursive:
def recursiveLast[T](xs: List[T]): Option[T] = xs match {
    case Nil => None
    case head :: Nil => Some(head)
    case head :: tail => recursiveLast(tail)
  }


Les tests

Voici le test de mes fonctions avec Specs2:

import org.specs2.mutable._

class P01Spec extends Specification {
  "P01.last1()" should {
    "return Some(8)" in {
      P01.last1(List(1, 1, 2, 3, 5, 8)) must_== Some(8)
    }

    "return Some(3)" in {
      P01.last1(List(1, 2, 3)) must_== Some(3)
    }

    "return Some(2.5)" in {
      P01.last1(List(3.23, 2.4, 5.0, 2.5)) must_== Some(2.5)
    }

    "return None" in {
      P01.last1(List()) must_== None
    }

    "return None" in {
      P01.last1(Nil) must_== None
    }
  }

  "P01.last2()" should {
    "return Some(8)" in {
      P01.last2(List(1, 1, 2, 3, 5, 8)) must_== Some(8)
    }

    "return Some(3)" in {
      P01.last2(List(1, 2, 3)) must_== Some(3)
    }

    "return Some(2.5)" in {
      P01.last2(List(3.23, 2.4, 5.0, 2.5)) must_== Some(2.5)
    }

    "return None" in {
      P01.last2(List()) must_== None
    }

    "return None" in {
      P01.last2(Nil) must_== None
    }
  }

  "P01.recursiveLast()" should {
    "return Some(8)" in {
      P01.recursiveLast(List(1, 1, 2, 3, 5, 8)) must_== Some(8)
    }

    "return Some(3)" in {
      P01.recursiveLast(List(1, 2, 3)) must_== Some(3)
    }

    "return Some(2.5)" in {
      P01.recursiveLast(List(3.23, 2.4, 5.0, 2.5)) must_== Some(2.5)
    }

    "return None" in {
      P01.recursiveLast(List()) must_== None
    }

    "return None" in {
      P01.recursiveLast(Nil) must_== None
    }
  }
}
The END!!! Mais avant de partir sachez que l'API de List offre une méthode (last) pour récupérer le dernier élément d'une liste et une autre (lastOption) pour récupérer un objet Option du dernier élément.
Bye!

En Scala toute classe peut avoir son compagnon!

Au commencement était le mot clé static...

Il arrive lors de la définition d’une classe d’avoir besoin de membres (champs ou méthodes) qui ne soient pas liés à une instance particulière de cette classe. En Java le mot clé static est utilisé pour qualifier ce genre de membres. Cela donne la chose suivante dans le code :

package com.nouhoum.tutorials;
 
import java.util.regex.Pattern;
 
public class PhoneNumber {
   private static Pattern PHONE_REGEX = Pattern.compile("0[1-9]([ .-]?[0-9]{2}){4}");
    
   private final String value;
    
   public PhoneNumber(String value) {
       this.value = value;
   }
    
   public String getValue() {
       return value;
   }
    
   public String toString() {
    return value;
   }
    
   public static boolean validate(PhoneNumber phoneNumber) {
       if(PHONE_REGEX.matcher(phoneNumber.value).matches()) 
return true;
else
         return false;
   }
}

Voici un exemple d’utilisation de la classe. Notez la syntaxe utilisée pour accéder aux membres statiques : le nom de la classe (ici PhoneNumber) un point (.) puis le membre statique. Nous verrons la syntaxe est similaire en Scala.
package com.nouhoum.tutorials;
 
import java.util.Arrays;
import java.util.List;
 
public class Main {
   public static void main(String[] args) {
      List<PhoneNumber> numbers = Arrays.asList(
            new PhoneNumber("12345"), new PhoneNumber("0102030405")
         );
          
      for(PhoneNumber number : numbers) {
         if(PhoneNumber.validate(number))
            System.out.println(number + " is valid");
         else
            System.out.println(number + " is not valid");
      }
   }
}

Les membres statiques n’étant liés à aucune instance particulière de la classe on les appelle membres de classe, ils sont plutôt liés à la classe elle-même.


Ouvrons une parenthèse (importante ;-)

Avoir des membres qui ne sont liés à aucune instance d’une classe n’est pas très orienté objet au goût de Scala (et du mien d’ailleurs ;-). Pour remédier à cette entorse aux règles de l’orienté objet Scala introduit un mot-clé object. Ce mot-clé permet de créer un singleton c’est-à-dire un objet dont une seule instance peut exister. Et c’est dans un objet singleton que vont tous les membres statiques (au sens Java) d’un type (class ou trait). Ainsi les membres autrefois statiques sont maintenant membres d’un objet singleton. Ouf l’O.O. est sauvé!!!

object Hello {
  def sayHello(name: String) = println("Hello " + name)
}
 
Hello.sayHello("Toto")

Remarquez que ce n’est pas vous qui créez l’instance de l’objet singleton, le runtime de Scala s’en occupe.


C’est quoi donc un objet compagnon?

Un objet compagnon est d’abord un object (c’est-à-dire un objet singleton). En outre il est associé à un autre type qui soit une classe soit un trait. Non cela ne veut pas dire que la classe va devoir se marier! Il s’agit en fait d’un objet qui a les caractéristiques suivantes:

  • il a le même nom que le type
  • il est défini dans le même fichier source que le type
  • il a accès aux membres privés du type qu’il accompagne

Maintenant voici l’implémentation du premier exemple en utilisant l’objet compagnon:
//File: PhoneNumber.scala
class PhoneNumber(val value: String) {
  override def toString = value
}
 
object PhoneNumber {
  val PhoneRegex = "0[1-9]([ .-]?[0-9]{2}){4}".r
 
  def validate(phone: PhoneNumber) = PhoneRegex.findFirstIn(phone.value) match {
 case Some(s) => true
 case None => false
  }
}

La méthode statique validate() ainsi que la chaîne statique représentant l’expression régulière ont été déplacées dans l’objet compagnon. Voici rapidement un exemple d’utilisation:
//File: Main.scala
val numbers = List(new PhoneNumber("123456789"), new PhoneNumber("0102030405"))
 
numbers.foreach { p: PhoneNumber =>
  if(PhoneNumber.validate(p))
 println(p + " is a valid french phone number")
  else {
    println(p + " is not a valid french phone number")
  }
}


Quelques usages de l’objet compagnon

Création d’objets
Les objets compagnons ont un autre usage assez intéressant : c’est la définition de “factory method” se débarrassant ainsi de l’écriture de new à chaque création d’objet.

val totoNumber = PhoneNumber("123456789")
Cette ligne est sémantiquement équivalente à la suivante :
val totoNumber = PhoneNumber.apply("123456789")

La méthode apply() est implémentée comme suit :
object PhoneNumber {
  def apply(value: String) = new PhoneNumber(value)
}

L'usage d'apply() dans les collections rend leur usage très agréable.

Les extracteurs
Pour faire vite les extracteurs vous donnent un moyen de faire du pattern sur des objets quelconques (c’est-à-dire qui ne sont pas des instances de case classes). C'est ici pour en savoir plus.


Conclusion

Les case classes tirent pleinement partie des objets compagnons car même si vous ne le voyez pas lorsque vous définissons une case classe le compilateur lui crée un objet compagnon avec une méthode unapply() (un extracteur très utile pour le pattern matching), une méthode factory apply() entre autres.

Pour faire simple, si vous êtes un java-iste, mettez vos méthodes et champs statiques dans l’objet compagnon.

Les extracteurs en Scala

Les extracteurs sont une fonctionnalité de Scala permettant de faire du pattern matching sur un objet et d’en extraire des propriétés. On a cette fonctionnalité gratuitement avec les case classes mais pas pour les autres types de classes.

Voici rapidement comment cela se passe avec les case classes. Supposons que nous avons une classe Person définie comme suit:

case class Person(firstName: String, lastName: String)

Nous avons une liste de personnes et souhaitons afficher seulement celles dont le nom est “Cohen” (une simple opération de filtrage quoi ;-)). Voici une solution avec le pattern matching et le fait que Person est une case classe.

val persons = List(Person("Elie", "Cohen"), Person("John", "Dupont"), 
                   Person("Steve", "Woz"), Person("Daniel", "Cohen"))
       
persons foreach { person: Person =>
  person match {
    case Person(_, "Cohen") => println(person)
    case _ => ()
 }
}

Cool tout ça! Mais si notre classe n’est pas une case classe que faisons-nous alors? Les extracteurs viennent à notre secours. Pour illustrer mes propos je vais prendre un exemple volontairement simpliste: une application qui affiche des e-mails.

trait Message

class EmailMessage(
  val from: String,
  val to: String,
  val subject: String,
  val content: String
 ) extends Message

Le composant chargé de l’affichage est défini comme suit:

trait MessageDisplayer {
  def display(messages: Seq[Message])
}

Voici une implémentation du trait MessageDisplayer:

class PrintStreamMessageDisplayer(out: PrintStream) extends MessageDisplayer {
  def display(messages: Seq[Message]) {
    messages foreach { message: Message =>
      message match {
        case EmailMessage(from, to, subject, content) =>  
          out.println("============= EMAIL =============")
          out.println("Subject: " + subject)
          out.println("From : " + from)  
          out.println(content)  
        case _ @ unknownMessageType =>
          out.println("Unkown message type" + unknownMessageType)
      }
    }
  }
}

Cette implémentation utilise un objet PrintStream pour afficher les messages. Notez l’usage du pattern matching sur EmailMessage. Ceci est possible grâce à la définition de la méthode unapply() dans l’objet compagnon (campanion object) de la classe EmailMessage.
Cette méthode est définie comme suit:

def unapply(email: EmailMessage) =
  Some(email.from, email.to, email.subject, email.content)

Elle prend en paramètre un objet de type EmailMessage et retourne objet Some contenant les propriété du message EmailMessage. Cette méthode est invoquée lorsque nous faisons :

message match {
        case EmailMessage(from, to, subject, content) =>  …
        ….
} 

Il existe une autre méthode apply() que l’on peut définir dans l’objet compagnon. Celle-ci permet de créer un objet EmailMessage avec la syntaxe :

val email = EmailMessage("tota@toto.com", 
                         "tata@toto.com", 
                         "Hello!", 
                         "Hi! How do you do?")

Remarquez l’absence de new dans le snippet précédent, il invoque la méthode apply() de l’objet compagnon avec les arguments fournis et celle-ci s’occupe des details de création de l’objet.
Le bout de code précédent est équivalent à

val email = EmailMessage.apply("tota@toto.com",
                         "tata@toto.com",
                         "Hello!",
                         "Hi! How do you do?") 

Code final d’EmailMessage:

class EmailMessage(
 val from: String,
 val to: String,
 val subject: String,
 val content: String
  ) extends Message

object EmailMessage {
  def apply(from: String, to: String, subject: String, content: String) =
 new EmailMessage(from, to, subject, content)
  def unapply(email: EmailMessage) =
 Some(email.from, email.to, email.subject, email.content)
}

PrintStreamMessageDisplayer et son campanion object:

class PrintStreamMessageDisplayer(out: PrintStream) 
     extends MessageDisplayer {
  def display(messages: Seq[Message]) {
    messages foreach { message: Message =>
      message match {
        case EmailMessage(from, to, subject, content) =>  
          out.println("============= EMAIL =============")
          out.println("Subject: " + subject)
          out.println("From : " + from)  
          out.println(content)  
        case _ @ unknownMessageType =>
          out.println("Unkown message type" + unknownMessageType)
      }
    }
  }
}

object PrintStreamMessageDisplayer {
  def apply(out: PrintStream) = new PrintStreamMessageDisplayer(out)
}

Enfin le code pour tester tout ça:
object ExtractorsTutorial extends App {
  val emails = Seq(
    EmailMessage("tota@toto.com", "tata@toto.com", "Hello!", "Hi! How do you do?"),
    EmailMessage("john.doe@noname.com", "jane@who.com", "Hello!", "Dear Jane! Who am I ?"),
    EmailMessage("madmax@domain.com", "borat@example.com", "Go", "Get out the way."))

    val displayer = PrintStreamMessageDisplayer(Console.out)
    displayer.display(emails)
}

Son exécution donne:
============= EMAIL =============
Subject: Hello!
From : tota@toto.com
Hi! How do you do?
============= EMAIL =============
Subject: Hello!
From : john.doe@example.com
Dear Jane! Who am I ?
============= EMAIL =============
Subject: Go
From : madmax@domain.com
Get out the way. 

Fin de voyage pour cette première rencontre avec les extracteurs.

Introduction à Akka

Dans ce billet, je vous présente Akka, un framework pour écrire des applications concurrentes, scalabes et robustes. Nous allons écrire une petite application qui reprend l'exemple de mon billet sur les expressions régulières. Il s'agit d'une application qui prend en entrée un ensemble de fichiers et les parse pour en extraire des numéros de téléphone suivant le plan de numérotation français. L'API d'Akka est disponible à la fois en Java et en Scala.

Quelques mots sur Akka et sa philosophie


Les concepteurs d'Akka sont partis du constat qu'écrire une application concurrente et scalable est difficile, manipuler directement des threads et des verrous (locks) est difficile pour nous fournir des abstractions comme les acteurs ou la STM (Software Transactional Memory) pour aider dans cette tâche. En outre Akka adopte une attitude particulièrement intéressante de la gestion des erreurs en disant la chose suivante:

  • Les erreurs sont inévitables - apprenons à les gérer au lieu d'essayer de les empêcher d'arriver. Ce qui donne dans la langue de Shakespeare: "Let it crash or embrace failure". En savoir plus: StackOverflow ou c2.com.
Cette philosophie, plus des outils comme l'API des Acteurs et celle de la STM simplifient grandement l'écriture d'applications concurrentes, scalables et robustes avec Akka.
Dans cet article nous mettrons en pratique le modèle des acteurs (les autres points seraient traités dans d'autres billets à venir).

Les acteurs


Le modèle des acteurs offre un modèle "simple" de faire communiquer différents processus par échange asynchrone de messages grâce à une abstraction plus haut niveau que celle que nous rencontrons avec l'API des threads et des verrous. Nous le verrons avec notre application le modèle des acteurs est particulièrement adapté à l'exécution de tâches indépendantes. Une application conçue avec ce modèle est constituée d'acteurs qui communiquent de manière asynchrone par échange de messages immuables. Un acteur n'expose pas son état interne au monde extérieur, il le garde jalousement pour lui seul évitant ainsi la nécessité de synchroniser lors de l'accès à des données partagées et les erreurs courantes comme les "dead locks" ou autres "race conditions". Chaque acteur dispose d'une boîte à messages qui contient les messages que le monde extérieur lui a envoyés. La vie d'un acteur est relativement routinière :  il reçoit des messages, fait des traitements, envoie des messages. C'est presque tout ce qu'il sait faire.
Voici comment implémenter un acteur avec l'API d'Akka: on étend le trait akka.actor.Actor et implémente la méthode receive.

import akka.actor.Actor

class MyActor extends Actor {
  def receive = {
    case "Hello" => println("Hello there!")
    case _ => println("What to do?")
  }
}
Que fait cet acteur? Il affiche "Hello there!" lorsqu'il reçoit le message "Hello". Pour tout autre message différent de "Hello" il affiche "What to do?". Pigé? Oui oui!

Pour créer une instance de notre acteur nous nous servons de la méthode akka.actor.Actor.actorOf. Cette méthode retourne un objet immuable de type akka.actor.ActorRef.

import akka.actor.Actor.actorOf

val myActor = actorOf[MyActor]

On peut aussi créer et démarrer un acteur en même temps:

import akka.actor.Actor.actorOf

val myActor = actorOf[MyActor].start()

Attends! Comment j'envoie un message à mon acteur? Beh il suffit d'invoquer la méthode ! (bang) sur l'objet de type ActorRef obtenu lors de la création de l'instance de notre classe MyActor. Voyons par exemple comment envoyer "Hello" à myActor:

import akka.actor.Actor.actorOf

val myActor = actorOf[MyActor].start()
myActor ! "Hello" //Send "Hello" message to the actor

Une dernière pour la route: la création d'un acteur anonyme:

import akka.actor.Actor
import Actor._

val myActor = actorOf(
  new Actor {
    def receive = {
      case "Hello" => println("Hello Toto!!")
      case _ => println("Oops!")
    }
  }
).start()  

myActor ! "Hello"

Dans l'application d'exemple nous allons implémenter deux types d'acteurs:

  • Un Master qui distribue les tâches entre les Workers et agrège les résultats pour créer le résultat final.
  • Des Workers qui effectuent les tâches c'est-à-dire parsent le contenu des fichiers


La supervision


La philosophie "Let it crash" implique la nécessité de pouvoir détecter les crashes des composants de l'application. En clair nous devons "monitorer", superviser le système et prendre les décisions qui s'imposent en cas de panne d'un ou de plusieurs composants. Pour cela il faut choisir quels composants de l'application doivent être supervisés et quels autres jouent le rôle de superviseurs. Une fois ce choix opéré nous établissons les liaisons entre composants superviseurs et ceux supervisés.
A ce jour, Akka offre deux stratégies de supervision

  • OneForOne: Dans cette stratégie le superviseur redémarre seulement le composant ayant crashé les autres n'étant pas impactés.
  • AllForOne:Ici contrairement à la stratégie précédente lorsqu'un composant supervisé tombe, celui-ci ainsi que tous les autres sont redémarrés.

Comme on peut le voir sur la figure ci-dessus un superviseur peut être, à son tour, supervisé.
Référez-vous à la page d'Akka pour plus d'information.

Le choix de la stratégie de supervision et celui des composants à superviser dépendent grandement du type d'application.

La STM


Un autre outil dans la boite à outils d'Akka est la STM (Software Transactional Memory). La STM offre une alternative à la synchronisation basée sur les verrous. Elle est utile pour contrôler l'accès à une zone mémoire partagée par plusieurs acteurs. Elle traite la mémoire comme une base de données et on retrouve la syntaxe habituelle pour les transactions: begin, commit et rollback en cas de souci. Pour supporter la STM Akka n'est pas parti de zéro, il s'est basé sur le projet Multiverse. C'est un sujet qui reste à creuser pour moi j'en dis pas plus donc.

L'application PhoneNumberSearchApp


Voyons maintenant en image l'application que nous allons développer:

Comment marche cette application? Le client dispose d'un ensemble de fichiers dont il souhaite extraire des numéros de téléphone (je dois avouer que cette application m'a été commandée la société Spamming Inc.). Il soumet donc ces fichiers à notre super application qui accomplit cette tâche si gratifiante. Puisque les fichiers peuvent être parsés indépendamment et parallèlement les uns des autres nous créons une tâche par fichier. Les tâches sont traitées suivant le pattern Master/Worker avec un Master qui distribue les tâches entre les Workers qui qant à eux parsent le contenu des fichiers.


Un mot de Simple Build Tool - SBT

Nous allons utiliser SBT qui est un outil de build pour Scala. Pour avoir avoir une introduction en règle à SBT rendez-vous sur sa page officielle.
Procédez comme suit pour créer le projet SBT:

  1. Créez un répertoire PhoneNumberSearchApp, placez-vous dedans et invoquez la commande sbt.
    moi@pc:~/PhoneNumberSearchApp$ sbt
    
    Puis suivez les instructions.
    Voici l’arborescence du projet que l'on obtient au final.
  2. Nous allons maintenant configurer notre projet SBT en créant sa classe de définition dans le fichier project/build/PhoneNumberSearchAppProject.
    import sbt._
    import de.element34.sbteclipsify._
    
    class PhoneNumberSearchAppProject(info: ProjectInfo) 
        extends DefaultProject(info) 
        with Eclipsify 
        with AkkaProject
    
    Remarquez nous ajoutons en même temps le support d'Akka et d'Eclipify qui est un plugin SBT pour Eclipse.
    Ensuite nous créons la classe project/plugins/Plugins.scala qui contiendra les définitions des plugins Akka et Eclipsify.

    import sbt._
    
    class Plugins(info: ProjectInfo) extends PluginDefinition(info) {
      //Add Eclipsify plugin
      lazy val eclipse = "de.element34" % "sbt-eclipsify" % "0.7.0"
      val akkaRepo = "Akka Repo" at "http:akka.io/repository"
      //Add Akka plugin
      val akkaPlugin = "se.scalablesolutions.akka" % "akka-sbt-plugin" % "1.1"
    }
    
    
  3. Ensuite faite tapez les commandes:
    moi@pc:~/PhoneNumberSearchApp$ sbt update 
    moi@pc:~/PhoneNumberSearchApp$ sbt eclipse
    
    A partir de là vous avez les dépendances Akka et vous pouvez aussi importer votre projet dans Eclipse.


Maintenant les mains dans le cambouis

Je vous sens impatient! Mais voici le moment tant attendu arrivé: le code!! Ça va aller très vite notamment grâce à l'expressivité de Scala.
Nous avons deux types de messages dans notre application: Ceux qui représentent une tâche à exécuter et ceux qui contiennent les résultats fournis par les Workers. Nous définissons les messages comme des case classes.

abstract class Message
case class Task(fileContent: String) extends Message
case class Result(numbers: Set[String]) extends Message
Chaque Worker met les numéros qu'il a trouvés dans une collection de type Set.

Maintenant voici le code des Workers:

/**
* A worker parses a file and returns found phone numbers as
* a set to the master actor.
*/
class Worker extends Actor {
  val phoneRegex = "0[1-9]([ .-]?[0-9]{2}){4}".r
 
  def receive = {
    case Task(fileContent) =>
      println("Worker doing hard work!!...")
      var phoneNumbers = Set[String]()
      
      for(phoneNumber <- phoneRegex findAllIn fileContent) {
        phoneNumbers += phoneNumber
      }
      self reply Result(phoneNumbers)
  }

  override def preStart() = {
    println("=> Worker pre start step!!")
  }

  override def postStop() = {
    println("=> Worker post stop step!!")
  }
}

Vous remarquerez les méthodes preStart() et postStop() qui sont des "hooks" (callbacks) où vous placez du code qui s'exécute avant le démarrage d'un acteur et après son arrêt.

Le Worker extrait le contenu du fichier sous forme de String et lui applique l'expression régulière fournie et renvoie le résultat à l'acteur qui lui a envoyé le message Task. La ligne qui envoie la réponse est:

self reply Result(phoneNumbers)

Le code du Master étant relativement complexe nous l'introduisons petit à petit. Commençons d'abord par la création des Workers.

val workers = Vector.fill(nbOfWorkers)(actorOf[Worker].start())
Ici nous utilisons l'une des méthodes du "companion object" de la classe Vector pour créer et démarrer en même temps autant de Workers qu'on a de tâches à effectuer. Eh oui! Tout cela en une ligne!
Nous en arrivons maintenant au Load Balancer, le composant qui distribue les tâches entre nos Workers. Akka fournit une API qui facilite grandement la création d'un tel composant comme en témoigne la ligne suivante:
val loadBalancer = Routing.loadBalancerActor(CyclicIterator(workers))
La méthode akka.routing.Routing#loadBalancer prend en argument un objet de type akka.routing.InfiniteIterator qui est un itérateur sur un ensemble d'acteurs. Cet objet implémente l'algorithme de distribution des tâches aux Workers. Nous avons choisi akka.routing.CyclicIterator qui implémente le round-robin et convient parfaitement à notre besoin. Si vous avez un algorithme de load balancing particulier pensez à étendre le trait akka.routing.InfiniteIterator.

Le prochain bout de code lit le contenu des fichiers fournis en ligne de commande à notre application, crée les tâches puis crée et démarre l'acteur Master avant de lui envoyer les tâches.

val tasks =
   for {
    file <- args
    fileContent = Source.fromFile(file).mkString
   } yield Task(fileContent)

val master = actorOf(new Master(tasks.length)).start()

val response = master !! tasks

Remarquez l'usage du double bang (!!) pour envoyer les tâches au Master. Cette méthode permet d'envoyer un message à un acteur et bloque l'appelant sur l'attente de la réponse.

Le Master forwarde les tâches aux Workers en passant par le Load Balancer. Il conserve aussi une référence sur l'expéditeur du message afin de pouvoir lui répondre plus tard quand toutes les réponses intermédiaires seront arrivées:

sender = self.senderFuture

for (task <- tasks) {
  loadBalancer ! task
}
Voici ce que fait le Master à la réception des résultats intermédiaires:
def receive = {
  case Result(intermediateNumbers) =>
    phoneNumbers = phoneNumbers ++ intermediateNumbers
    responseCount += 1

    if (responseCount == nbOfWorkers) {
      senderFuture.foreach(_.completeWithResult(phoneNumbers))
      loadBalancer ! Broadcast(PoisonPill)
      loadBalancer ! PoisonPill
      self.stop()
    }
  
  case tasks: Array[Task] => ...
}
Le Master agrège les résultats intermédiaires dans une collection et une fois que tous les Workers ont fini leur travail, le résultat final est renvoyé à l'application grâce à la méthode completeWithResult du Future de l'appelant:
senderFuture.foreach(_.completeWithResult(phoneNumbers))
Puisque c'est bien élevé de nettoyer derrière soi, le Master envoie d'abord un message spécial PoisonPill à tous les Workers via le load balancer, puis le même message à ce dernier avant de s'arrêter soi-même. Le PoisonPill (un vrai poison cette chose) a pour effet l'arrêt de l'acteur récepteur. Les 3 lignes correspondantes sont:
senderFuture.foreach(_.completeWithResult(phoneNumbers))
loadBalancer ! Broadcast(PoisonPill)
loadBalancer ! PoisonPill

Enfin le bout de code qui affiche le résultat final sur la sortie standard:

val response = master !! tasks

response match {
  case Some(numbers) =>
    numbers match {
      case numbersAsIter:Iterable[Any] =>
        println(numbersAsIter mkString("\n"))
      case _ @response =>
        println("Unknown response format : " + response)
    }   
       
  case None => println("No phone number found")
}
Nous utilisons la méthode scala.collection.immutable.Iterable#mkString pour afficher un numéro par ligne.

Allons exécuter ce programme!

Placez-vous dans le répertoire du projet et lancez SBT puis lancer la commande suivante:

>run test.txt test1.txt
Nous obtenons une sortie similaire à ce qui suit:
> run test.txt test1.txt
[info] 
[info] == copy-resources ==
[info] == copy-resources ==
[info] 
[info] == compile ==
[info]   Source analysis: 1 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[warn] there were 1 unchecked warnings; re-run with -unchecked for details
[warn] one warning found
[info] Compilation successful.
[info]   Post-analysis: 21 classes.
[info] == compile ==
[info] 
[info] == run ==
[info] Running com.nouhoum.akka.PhoneNumberSearchApp test.txt test1.txt
=========================
AKKA_HOME is defined as [/home/toto/tools/akka-1.1/akka-core-1.1], loading config from [/home/toto/tools/akka-1.1/akka-core-1.1/config/akka.conf].
=> preStart() of the Worker
=> preStart() of the Worker
=> preStart() of the Master
Waiting for the master response....
Received task list size = 2
Worker doing hard work!!...
Worker doing hard work!!...
====The following numbers has been found====
0102030405
08 05 05 05 03
0405050503
09 45 33 54 22
06-05-05-05-03
0304458966
0404458966
Total time : 220 ms
=========================
=> postStop() of the Master
=> postStop() of the Worker
=> postStop() of the Worker
[info] == run ==
[success] Successful.
[info] 

Code source et conclusion

Je souhaitais, à travers cet article, vous donner un aperçu d'Akka. L'exemple traité ici permet d'avoir une idée de la puissance d'Akka. J'espère qu'il vous a donné envie d'explorer l'outil. Quant à moi je continue à l'explorer et vous ferais découvrir ici mes futures découvertes et aventures avec Akka.

Le code source est disponible ici.

Happy hAkking!

© Nouhoum TRAORE.. Fourni par Blogger.