Mis à jour le mardi 7 mars 2017
  • Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Pourquoi Scala ?

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Si vous êtes un programmeur expérimenté et que vous ne voulez pas aller chercher sur Google pour voir ce que propose Scala, alors cette annexe vous intéressera.
La première sous-partie vous présentera Scala d'une manière générale (historique, typage...) et la deuxième vous montrera quelques points forts de Scala par rapport aux autres langages que vous connaissez (principalement Java).

Présentation générale

Scala est un langage récent, à typage statique, qui compile vers du bytecode Java et qui tourne sur la JVM (il peut aussi être compilé en MSIL et utiliser tout le framework .Net mais, étant donné que je n'utilise aucun des langages de Microsoft, ceci ne sera pas abordé ici). Son nom est la concaténation de l'expression anglaise « Scalable Language », qui veut dire langage évolutif ou « langage qui peut être mis à l'échelle ».

Image utilisateur

Il a été conçu à l'École Polytechnique Fédérale de Lausanne (EPFL) pour être un langage polyvalent, puissant et adapté aux petits scripts comme aux grands logiciels.
Martin Odersky, son créateur, est un professeur de programmation à l'EPFL. Il a longuement cherché à unifier la POO et la PF (qui sont en général des paradigmes orthogonaux, sauf par quelques essais peu fructueux comme OCaml − 90 % des développeurs OCaml n'utilisent que son coté fonctionnel). Le sujet de ses recherches était que ces deux paradigmes sont deux faces différentes d'une même pièce. Afin de prouver ses propos, il a essayé de créer plusieurs langages comme Pizza, GenericJava (une partie de ce langage a été fusionnée avec Java pour donner les classes et méthodes génériques de Java 5) et Functional Nets. Il a commencé le développement de Scala en 2001 et a publié la première version stable vers la fin de 2003.

Attends ! Si ton langage tourne sur la JVM et compile en bytecode Java, autant utiliser le langage Java lui-même, n'est-ce pas ?

Eh bien non ! Les principales raisons du succès de Java sont :

  • le fait qu'un programme Java peut tourner sur n'importe quel OS grâce à une machine virtuelle : la JVM ;

  • sa grande API : une bibliothèque standard géante (parmi les plus grandes au monde), qui nous épargne le coup de la recherche des bibliothèques externes (GUI, multi-thread, réseau... tout est déjà à votre disposition :) ) ;

  • son coté (presque) tout objet (la POO était à la mode au moment de l'apparition du langage).

Tous ces avantages sont dus à la plateforme Java (JVM et API) et non pas au langage. On a donc créé une multitude de langages qui tournent sur la JVM pour les gens qui n'aiment pas Java (pour sa verbosité, par exemple), ou qui veulent changer un peu d'air. Parmi ces langages, on citera les plus célèbres :

  • Clojure ;

  • Groovy (++) ;

  • Scala.

Étant donné que le seul langage statiquement typé dans cette liste est Scala, c'est lui qui a les plus grandes chances de remplacer Java, ou au moins de cohabiter avec lui, dans un futur lointain bien évidemment.

D'une part, Scala est un langage objet pur. Les classes, les constructeurs et l'héritage seront donc tous au rendez-vous mais avec une syntaxe beaucoup moins verbeuse que celle de Java, C# ou C++. En fait, c'est sa syntaxe simple, élégante et pratique qui m'a permis d'écrire un tutoriel pour des débutants complets en programmation qui, pour une fois, commence par apprendre la POO et non pas la programmation procédurale.
D'autre part, Scala supporte le paradigme fonctionnel, les fonctions sont des objets tout comme les chaines et les entiers donc elles peuvent être passées en arguments à d'autre méthodes (on parle de méthodes d'ordre supérieur dans ce cas) ou encore stockées dans des variables. Avec des bonnes fonctionnalités héritées de Haskell on peut (enfin ^^ ) dire adieu aux NullPointerException.

Fonctionnalités principales

Notation infixe et DSL

En Scala la notion d'opérateur n'existe pas, il n'y a que des méthodes. Par contre on peut simuler les opérateurs parce que :

  • il y a une syntaxe élégante pour appeler les méthodes à un seul argument : au lieu de o1.methode(o2) on peut écrire o1 methode o2, c'est cette deuxième notation qui est appelée notation infixe ;

  • on peut utiliser des noms exotiques pour les noms de méthodes : +, ::, **... sont des identificateurs valides en Scala.

Du coup, il suffit de nommer une méthode « + » et de l'utiliser en notation infixe pour simuler l'opérateur « + » : o1 + o2. :magicien: En fait, puisque tout est objet, même 1 + 2 est équivalent à 1.+(2).

Tout ça rend la création des DSL (Domain Specific Language) très facile. Les deux principaux DSL dans l'API standard sont ceux des Actors et des Parsers.

Les Actors

La programmation parallèle (multi-thread) est devenue un must avec l'apparition des ordinateurs multi-cores. Le langage Erlang reste une référence dans ce domaine grâce à l'approche très élégante qu'il utilise pour gérer ce genre d'applications. Scala a « recopié » ce modèle et, à l'aide d'un DSL, il lui a donné une utilisation similaire à celle d'Erlang pour faciliter la migration des Erlangueux vers Scala. Comparez vous-mêmes ces deux codes :

loop(Fun) ->
    receive
        {core, charger, NewFun} -> ...

        quit -> ...

        Else -> ...
    end.
def act() {  
    loop {  
      receive {  
        case (core, charger, NewFun) =>  //...

        case Quit =>  //...

        case Else =>  //...  
      }  
    }  
  }
Les Parsers

On a parfois besoin de parser des expressions, des entrées de l'utilisateur (par exemple, dans un logiciel de dessin de courbes de fonctions on est amené à évaluer la chaine donnée par l'utilisateur (« sin(x) * x / ch(x) »). Ceci mène toujours à l'utilisation de bibliothèques externes telles que Lex et Yacc.
En Scala, un DSL se charge de tout ça. Prenons par exemple cette grammaire qui représente des opérations arithmétiques (exemple extrait du livre de Martin Odersky) :

expr ::= term {"+" term | "-" term}
term ::= factor {"*" factor | "/" factor}.
factor ::= floatingPointNumber | "(" expr ")".

Pour implémenter cette grammaire en Scala il suffit de remplacer :

  • ::= par Parser[Any] = ;

  • les espaces par « ~ » ;

  • les accolades (qui désignent un pattern répété 0 fois ou plus) par rep(...).

ce qui donne :

class Arith extends JavaTokenParsers {   
    def expr: Parser[Any] = term~rep("+"~term | "-"~term)
    def term: Parser[Any] = factor~rep("*"~factor | "/"~factor)
    def factor: Parser[Any] = floatingPointNumber | "("~expr~")"
  }

Simple comme bonsoir !

Voici un exemple que j'ai fait pour le Lisp-like de l'atelier p'tit langage. Mon code gère les calculs arithmétiques, la définition et l'utilisation des variables avec évaluation et affichage (je le mets en secret parce qu'il risque d'être de mauvaise qualité :euh: ).

class Lisp extends JavaTokenParsers {
  val fpt = floatingPointNumber

  def factor: Parser[Any] = (
    fpt ^^ {case x => x.toFloat}    
    | expr
    | ident
  )
  
  def expr: Parser[Any]= (
    "("~>op~factor~factor<~")" ^^ {
      case op~f~g if ! (List("define","let!","if") contains op) =>
        op match {
          case "+" => getFloat(f) + getFloat(g)
          case "-" => getFloat(f) - getFloat(g)
          case "*" => getFloat(f) * getFloat(g)
          case "/" => getFloat(f) / getFloat(g)
          case "=" => getFloat(f) == getFloat(g)
        }
      case ("define"|"let!")~(f: String)~g => vars +=
        f -> getFloat(g)
        "defined " + f +" = " + vars(f)
      case _ => "Erreur"
    }
  )

  def arithOp = "+" | "-" | "*" | "/"
  def op =  arithOp | "define"  

  val vars = MMap[String,Float]()

  def getFloat(a: Any): Float = a match {
    case b: Float  => b
    case b: String if vars.keySet contains b => vars(b)
    case b: String =>  getFloat(parseAll(expr,b).get)
    case _         =>  Float.NaN
  }
}

Exemple d'utilisation :

object Main extends Lisp{
  def lisp(s: String) = println(parseAll(expr,s).get)
  def main(args: Array[String]){
    lisp("(define x (+ 2 5))")
    lisp("(+ (- 5 2) (* x 4))")
  }
}
defined x = 7.0
31.0

Le filtrage de motifs (Pattern Matching)

C'est une structure de contrôle qu'on trouve dans la plupart des langages fonctionnels. Vous pouvez le considérer comme un « switch++ », voici un exemple :

def f(a: Any) = a match { // Any est l'équivalent de Object de Java
  case 0 | 1                 => println(a)
  case n: Int if(n % 2 == 0) => println("entier pair")
  case _: String             => println("c'est une chaine")
  case _                     => println("c'est autre chose")
}

Ce qu'on a utilisé plus haut dans les exemples des DSL était du filtrage de motifs aussi. Il est utilisé partout dans le langage, notamment dans la gestion des exceptions et des événements en Swing.

Exceptions

Les exceptions ne doivent pas être obligatoirement attrapées comme en Java (le système des exceptions de Java à été longuement critiqué), voici à quoi ça ressemble en Scala :

try {
  //opération qui peut lancer une exception
} catch {
  case  _: IndexOutOfBoundsException | _: NullPointerException => println("Erreur fatale !")
  case e: ArithmeticException       => e.printStackTrace
  case e                            => throw e
} finally {
  //
}

Notez l'existence du mot-clé « throw » prévu pour Java 7 (ou pas).

Événements en Swing

Dans la bibliothèque des GUI de Java, Swing, les événements sont gérés à l'aide des listeners. Malgré leur puissance, ils rendent l'utilisation de Swing difficile pour les débutants. Dans la version de Scala, la gestion des événements se fait d'une manière très élégante en utilisant le filtrage de motifs :

reactions += {
  case ButtonClicked(b)       =>  //
  case KeyPressed(a, b, c, d) =>  //
  case MouseMoved(a, b, c)    =>  //
}

C'est de loin plus simple, plus pratique et plus « naturel ».

La fin des NullPointerException

Un des problèmes non repérables en Java lors de la compilation est celui des références nulles. Dès qu'on essaye d'appeler une méthode sur un objet qui vautnull, une exception (NullPointerException) est lancée, ce qui mène au plantage du programme. Scala possède des mécanismes qui réduisent la possibilité de finir avec cette exception. Ceci est réalisé avec :

  • le fait que toute variable doit être initialisée au moment de sa création (il ne faut pas l'initialiser à null quand même :p ) ;

  • le type Option, l'équivalent de Maybe de Haskell.

La classe Option est abstraite. Elle possède deux classes filles : Some et None. L'idée est la suivante : si on a une méthode de type de retour T susceptible de renvoyer null, on remplace le type de retour par Option[T] et on retourne Some(quelqueChose) dans le cas de succès et None dans le cas d'échec. Ceci va obliger l'utilisateur de la méthode à utiliser le filtrage de motifs pour extraire quelqueChose, donc il n'oubliera pas de traiter le cas de None comme l'on fait avec null.
Voici un exemple d'une fonction qui calcule la racine carrée d'un Double et retourne un Option[Double] :

def racine(d: Double) : Option[Double] = {
  if (d >= 0) return Some(math.sqrt(d))
  else return None
}

Et un cas d'utilisation :

var x = racine(5)
x match {
  case Some(a) => println("La racine de " + x + "est " + a)
  case None    => println("On ne peut pas calculer la racine de " + x)
}

L'exemple est débile mais c'est juste pour comprendre comment faire. La classe Option est utilisée massivement dans l'API des collections de Scala. Par exemple lorsqu'on demande la valeur associée à une clé, dans une Map, on reçoit Some(valeur) si la clé existe dans la Map et None dans le cas contraire. Ça évite donc plein d'erreurs que font surtout les débutants en Java.

Modularité

Les packages

Un des problèmes de Java est le manque de modularité (d'où l'introduction des super-packages dans Java 7) : dès que l'on veut qu'une classe ou méthode soit accessible à partir d'un autre package il va falloir la déclarer publique, ce qui n'est ni très pratique, ni très propre.
En Scala, on peut créer des packages imbriqués :

package package1{ 
  package package11 {
    private[package1] class Classe1
  }
  package package12{
    package package121 {     
      private[package12] class Classe2
    }
    private class Classe3
  }
}

Ici Classe1 n'est publique que dans package1 et Classe2 ne l'est que dans package1.package12.

Les imports

On peut, en Scala, faire des imports n'importe où, ils ont la même portée que les variables. On n'importe pas que les classes mais on peut importer aussi les champs d'un objet :

val l = List[Int](5, 3, 8, 10)
  import l.length
  println(length) // length == l.length
  import l.{foldLeft => fold} // on importe la méthode l.foldLeft en la renommant en fold
  val somme = fold(0)(_ + _)

La programmation fonctionnelle

En Scala, les fonctions et les méthodes sont deux notions différentes. Les fonctions désignent ce qu'on appelle lambda ou closure dans d'autres langages, ce sont donc les fonctions anonymes. Leur syntaxe est simple et ressemble à la notation mathématique :

(x: Int) => 2 * x
(s: String, l: List[String]) => s :: l //on ajoute s au début de la liste

L'avantage des fonctions par rapport aux méthodes est qu'elles sont des objets. On peut donc les mettre dans des variables :

val fois2 = (x: Int) => 2 * x

Ou les passer en argument à d'autres méthodes (on parle dans ce cas d'une méthode d'ordre supérieur) :

/* cette méthode prend en arguments une liste d'entiers et une fonction
et affiche le résultat d'appel de la fonction sur chacun des membres de la liste */

def appliquer(l: List[Int], f: Int => Int) = for(n <- l) println(f(n))

//exemple d'utilisation
appliquer(List(1, 5, 8, 9), fois2)

Les méthodes d'ordre supérieur rendent l'utilisation des collections très simple.
Voici quelques exemples (ici « l » est une liste d'entiers) :

//calcul de la somme des éléments de l
l foldLeft(0)((x: Int, y: Int) => x + y)

//ou en plus compact
l foldLeft(0)(_: Int + _: Int)

//ou en utilsant reduceLeft
l reduceLeft (_: Int + _: Int)

// et en laissant Scala inférer les types (cf. plus bas pour l'inférence)
l reduceLeft(_ + _)

//ou en utilisant une méthode prédéfinie
l.sum

/* cette instruction supprime les entiers pairs de l, multiplie les impairs
par trois et calcule leur produit */
l filter (x: Int => x % 2 != 0) map (x: Int => 3 * x) product

//et en utilisant la notation compacte et l'inférence
l filter (_ % 2 == 0) map (3*) product

Conversions implicites

Les méthodes implicites permettent de convertir un objet d'une classe à une autre. C'est particulièrement utile lorsqu'on veut ajouter une méthode à une classe à laquelle on n'a pas accès.
Dans cet exemple, on va ajouter la méthode isPrime à la classe Int :

// on commence par définir la classe qui contiendra la nouvelle méthode
class MonInt(val i: Int) {
  def isPrime = i match {
    case 0 | 1            => false
    case 2                => true
    case _ if i % 2 == 0  => false
    case _                => Range(3, i / 2, 2) forall {i % _ != 0}
  }
}

object Main {
  // on crée les conversions implicites de Int vers MonInt et vice-versa
  implicit def IntToMonInt(i: Int) = new MonInt(i)
  implicit def MonIntToInt(mi: MonInt) = mi.i
  
  // et un exemple d'utilisation
  def main(args: Array[String]) {
    for(i <- 1 to 11) println(i.isPrime)
  }
}

Inférence des types

Scala est parfois appelé « le langage dynamique statiquement typé » parce qu'il peut inférer (« deviner ») quelques types tout seul.
Par exemple l'équivalent de ce code Java :

HashMap<String, Double> map = new HashMap<String, Double>() ;
map.put("1.0", 1.0);
map.put("2.3", 2.3);

est :

val map: Map[String, Double] = Map[String, Double]("1.0" -> 1.0, "2.3" -> 2.3)

D'abord, Scala peut inférer la classe de map à partir de son initialisation. On peut donc écrire :

val map = Map[String, Double]("1.0" -> 1.0, "2.3" -> 2.3)

Aussi, Scala peut inférer les paramètres de la Map à l'aide des valeurs mises dedans ("1.0" -> 1.0) :

val map = Map("1.0" -> 1.0, "2.3" -> 2.3)

Du coup, la quantité du code est réduite de moitié.

Il existe d'autres aspects de l'inférence tel que l'inférence du type de retour des méthodes et celle des types des fonctions.

Autres fonctionnalités

  • Types membres et types anonymes.

  • Types paramétrés, covariance, non-variance et contra-variance.

  • Héritage multiple.

  • Support direct d'XML (les balises XML sont des objets aussi).

  • Encapsulation avancée.

  • Pattern singleton au lieu des méthodes statiques (qui ne sont pas très OO).

  • Valeurs par défaut des arguments (comme en C++).

  • Évaluation paresseuse.

  • Structures de données paresseuses et infinies.

  • ...

Je vais finir cette sous-partie en vous donnant quelques exemples qui comparent des codes Java à des codes Scala, rien que pour vous donner une idée de la syntaxe du langage et de la puissance de cette union fonctionnel-objet.

Exemple 1

On crée ici une classe Personne, dont les membres sont un nom, un prénom et un âge (et les mutateurs/accesseurs associés, utiles pour un JavaBean par exemple) avec un constructeur par défaut et un constructeur secondaire.

En Java, ça donne ceci :

 

public class Personne {
    private final String nom, prenom ;
    private int age ;
    
    public Personne(String nom, String prenom, int age) {
        this.nom = nom ;
        this.prenom = prenom ;
        this.age = age ;
    }
    
    public Personne(String nom, String prenom) {
        this(nom,prenom,0) ;
    }
    
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getNom() {
        return nom;
    }

    public String getPrenom() {
        return prenom;
    }
}

Et en Scala, ceci :

 

class Personne(@BeanProperty val nom: String, @BeanProperty val prenom: String, @BeanProperty var age: Int = 0)

:waw:
Impressionnant, n'est-ce pas ?

Exemple 2

On va finir par créer une classe qui représente un Graph non-orienté. Le constructeur prend deux paramètres : une liste de chaines qui représente les sommets et un dictionnaire (Map) qui associe à chaque arrête la paire de sommets associée, on vérifie aussi l'appartenance de chacun des éléments de la paire à la liste des sommets :
Le code Java :

class Graph {

    private final HashSet<String> v;
    private final HashMap<String, Pair<String, String>> e;

    public Graph(HashSet<String> v, HashMap<String, Pair<String, String>> e)
            throws Exception {
        this.v = v;
        this.e = e;
        for (Pair<String, String> p : e.values()) {
            if (!(v.contains(p.e1) && v.contains(p.e2))) {
                throw new Exception();
            }
        }
    }

    class Pair<T0, T1> {

        public final T0 e1;
        public final T1 e2;

        public Pair(T0 e1, T1 e2) {
            this.e1 = e1;
            this.e2 = e2;
        }
    }
}

Et l'équivalent en Scala :

class Graph(val v: List[String], private val e: Map[String, (String, String)]) {
  require(e.values forall {case (x, y) => (v contains x) && (v contains y)})
}

C'est moins long et plus lisible (ça ressemble plus à une phrase en anglais ;) ).

J'espère que vous êtes maintenant convaincus que Scala est un langage intéressant et que chaque développeur doit l'ajouter à sa collection.
Je vous assure que Scala a changé tout mon style de programmation, même mes codes Java ne sont plus les mêmes qu'auparavant. Si j'écris ce tutoriel (qui est d'ailleurs le premier big-tuto français de Scala), c'est pour la simple raison que j'aime Scala et que je veux le voir se propager dans le monde de la JVM (c'est quasi impossible qu'il remplace totalement Java mais ils pourront vivre ensemble, tout comme C#, VB.Net et F# sur la plateforme .Net).

Exemple de certificat de réussite
Exemple de certificat de réussite