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

Leave a Comment
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

Leave a Comment
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

Leave a Comment

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

Leave a Comment

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

Leave a Comment

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!

Leave a Comment

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

Leave a Comment

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

2 comments
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!

Scala et les expressions régulières

Leave a Comment
Les expressions régulières sont puissantes et utilisées pour faire des choses comme la recherche, la suppression ou le remplacement de texte. Je vous propose dans ce billet d’explorer leur support dans Scala à travers quelques exemples.

Les regex avec le package java.util.regex


Étant donné que les API de Java sont accessibles à Scala vous pouvez utiliser le package java.util.regex de Java dans votre programme pour traiter les expressions régulières. Pour commencer prenons un exemple: écrire un programme qui vérifie si une chaîne de caractères donnée contient ou non “toto”. Le test devrait être positif avec “Oh ca va toto?” ou “toto tire nama.” et négatif avec “Tato” ou “Scalable Language”.
Le programme demande à l’utilisateur de taper quelque chose dans la console puis vérifie ce qu’il a tapé contient “toto”. Voici le code avec l’API de Java :

import java.util.regex.{Matcher, Pattern}

val Exit = "exit"
var input = ""
val regex = "toto"
val pattern = Pattern.compile(regex)

while(input != Exit) {
 println("Please enter something... Type exit to quit")
 input = readLine()
 println("You entered = " + input)
 
 if(input != Exit) {
  if(pattern.matcher(input).matches()) println("Found some matches!!")
  else println("Ooops!! " + input + " does not match " + regex);
 }
}


Nous commençons par écrire notre regex (ici “toto”) puis nous le compilons en invoquant la méthode statique compile de la classe java.util.regex.Pattern.  
Nous obtenons ainsi une représentation compilée de notre expression régulière.
val pattern = Pattern.compile(regex)
Ensuite nous testons si la chaîne de caractères fournie par l’utilisateur matche (contient “toto”) notre expression régulière en examinant la variable hasMatches :
boolean hasMatches = pattern.matcher(input).find()

Les regex avec la class scala.util.matching.Regex


Voyons maintenant la même chose avec l’API de Scala :
import scala.util.matching.Regex

val Exit = "exit"
val regex = new Regex("toto")
var input = ""

while(input != Exit) {
 println("Please enter something... Type \"exit\" to quit")
 input = readLine
 println("You entered = " + input)

 if(input != Exit) {
  regex findFirstIn input match {
   case Some(s)    => println("Found some matches!!")
   case None     => println("Oopps!!! No match found.")
  }
 }
}
Dans cette version nous avons utilisée l’API de Scala pour les expressions régulières notamment la classe scala.util.matching.Regex. Ensuite nous invoquons la méthode findFirstIn de l’objet Regex qui renvoie un objet de type Option. Si un match est trouvé nous obtenons Some(s) dans le cas contraire nous avons un None.

Avant de voir un exemple plus élaboré avec les expressions régulières voici une façon succinte de créer un objet de type Regex en Scala:
val regex = "toto".r
Comment cela se passe-t-il? Vous écrivez votre expression régulière comme une chaîne de caractères sur laquelle vous évoquez la méthode r et vous obtenez un objet java.util.matching. En action cela donne : 
scala> val reg = "toto".r
reg: scala.util.matching.Regex = toto
Le script suivant prend en entrée un ensemble de fichiers et essaie d’en extraire des numéros de téléphone :
/*This script parses files given as inputs and tries to extract phone numbers 
from them. The regular expression is based on the French telephone numbering plan */

import scala.io.Source

if(args.length > 0) {
 val phoneNumberRegex = "0[1-9]([ .-]?[0-9]{2}){4}".r
 
 for(file <- args) {
  val input = Source.fromFile(file).mkString
  for(phoneNumber <- phoneNumberRegex findAllIn input) 
   println(phoneNumber) 
 } 
} else {
 Console.err.println("Oops! Please enter a file!")
 println("Script usage : phone_number_search.scala input_file")
}
L'exécution du script avec deux fichiers texte contenant des numéros téléphones donc le résultat suivant :
nouhoum@pc:~/tutorials$ scala phone_number_search.scala file1.txt file2.txt 
01 02 03 04 05
0203040506
03-44-55-76-00
09 03 44 56 88
09 04 44 56 88
09 03 44 56 98
0404050699
01 02 03 04 65
0503040506
Essayez! Ca marche! J’espère vous avoir donné envie d’explorer la manipulation des expressions régulières avec Scala.  A bientôt! 

Une brève introduction aux collections Scala avec les listes

1 comment
Les collections de Scala sont riches et puissantes. Elles font, sans doute, partie des "features" qui rendent Scala attrayant pour les développeurs. Elles démontrent aussi toute la puissance que Scala offre à ceux des librairies ou des "frameworks". En effet les collections de Scala ne sont pas contruites dans le langage lui-même; c'est une librairie que vous auriez vous pu écrire avec ce que Scala vous offre.
Dans ce billet il ne s'agit de rentrer dans les détails de fabrication des collections de Scala mais plutôt de nous placer du point de vue d'un utilisateur des collections. J'espère qu'à la fin vous verrez que Scala ne mérite pas toujours sa réputation de langage difficile d'accès. A mon humble avis un grand nombre de "features" avancées de Scala ne doivent pas être exposées à l'utilisateur final d'une API mais plutôt exploitées par les développeurs de librairies ou de frameworks. Le contraire risquerait de nuire à l'adoption de ce magnifique langage. Fermons cette longue parathèse et passons au sujet de ce billet: les collections de Scala vues au travers des listes.

Veuillez me signaler toute incorrection dans ce billet!

Pour exécuter les bouts de code de ce billet vous pouvez utiliser directement l'interpréteur interactif de Scla - REPL (Read-Evaluate-Print Loop).

Construction

La construction d'une liste en Scala est on ne peut plus simple. Par exemple comment créer une liste de noms.



Au regard de la sortie de l'interpréteur interactif REPL on se rend compte Scala on a fait l'inférence de type sur notre de noms et en a conclu qu'il s'agit d'une liste dont les éléments sont de type java.lang.String. Ceci va aider à l'écriture du code plus concis qu'on peut avec un langage comme Java.
Voyons si cela marche avec une liste de nombres! Voici le résultat:



Et ça marche aussi! Le type Int a été inféré.

Traverser une liste

Nous pouvons traverser une liste et appliquer une opération à chacun de ses éléments grâce à la méthode foreach dont voici la signature:



Pour utiliser cette méthode il suffit de lui passer une fonction qui sera application à chaque élément de la liste. Dans le code cela donne:



Filtrage

Les opérations de filtrage sont assez simple avec les  éléments de Scala. Supposons par exemple que nous souhaitons seulement récupérer les éléments positifs d'une liste de nombres. Nous ferons cela avec les méthode List.filter dont voici la signature.

Cette méthode applique la fonction passée en argument aux éléments de la liste. On obtient en retour une liste constituée des éléments de la liste initiale satisfaisant la condition posée par la fonction.

Voici comment obtenir une liste constituée seulement des éléments pairs de notre liste de nombres:

Pendant qu'on y est voici une autre méthode de List qui est du même acabit que filter:

Cette méthode fait l'opposé de ce que fait la méthode List.filter.

La méthode List.map
Cette méthode application une transformation, une fonction, à chaque élément d'une collection. Elle peut potentiellement changer le type des éléments de la collection, par exemple en passant d'une collection de String à une collection d'éléments de type Int.
Le listing suivant donne quelques cas d'usage de cette méthode. Suivez les commentaires dans le code.


Partitionnons notre liste
Si nous souhaitions "partionner" une collection suivant un critère donné? Cela est possible grâce à la méthode List.partition. Voici comment partionner une liste de nombres en nombres positifs  et négatifs. Dans le code cela donne...


La méthode foldLeft

Pour finir cette découverte des collections de Scala découvrons une dernière méthode de List. Voici sa signature

B est le type du résultat final et z est la valeur initiale utilisée pour l'opération. La fonction f est appliquée d'abord sur l'élément le plus à gauche de la liste et la valeur initiale puis sur le deuxième sur le deuxième élément le plus à gauche et le résultat de la première application, et ainsi de suite.
Passons à la pratique. Faisons la somme d'une liste de nombres positifs en utilisant la méthode foldLeft. Dans ce cas la valeur initiale est 0.


Pour finir
Les boucles sont omniprésentes lorsqu'on travaille avec les collections en Scala contrairement à Java.
L'aspect fonctionnel de Scala rend pratique la manipulation des collections.
Cet article n'était qu'une mise en bouche; une façon pour moi de vous donner envie d'explorer davantage les collections de Scala. C'est un sujet vaste que je continue à explorer.
J'apprécierai le signalement de toute erreur présente dans cet article!
© Nouhoum TRAORE.. Fourni par Blogger.