Le langage Scala
Bases du langage
Cette section a pour objectif de présenter les bases du langages Scala, afin que les morceaux de code utilisés pour illustrer les concepts plus complexes soient compréhensible.
Syntaxe et philosophie
Le langage Scala propose trois paradigmes de programmation :
- Le paradigme de programmation orientée objet, qui reprend la plupart des concepts de fonctionnement du langage Java
- Le paradigme de programmation impérative
- Le paradigme de programmation fonctionnelle. Bien qu'il s'oppose en tout point à la programmation impérative, Scala propose de travailler soit avec l'un, soit avec l'autre, soit avec les deux. Cependant, l'alliance des deux paradigmes n'apporte rien de plus et fait perde tous les avantages que procurent ces deux paradigmes
Le Scala est un langage à typage statique, c'est-à-dire que toutes les variables manipulées ont un type. Ce choix a été fait pour que les erreurs de programmation soient détectées à la compilation. Le langage Scala offre cependant les avantages du typage dynamique : lorsque le compilateur peut deviner le type d'une variable, il n'est pas nécessaire de la spécifier. Ce concept est expliqué en détails dans la section sur les variables ci-dessous.
En Scala, tout est vraiment objet contrairement au langage Java. En effet, les types primitifs du langage comme les entiers ou les booléens sont des classes (Int et Boolean, respectivement). Le langage Scala étant un langage objet, une variable d'un type peut accepter soit une instance de ce type, soit une instance d'une classe qui hérite de ce type.
Enfin, la syntaxe du langage Scala se veut la plus légère possible. Par exemple, les points-virgule à la fin d'une instruction n'est pas obligatoire. D'autres exemples de caractères et de mots-clés facultatifs en Scala sont donnés dans la suite de cette partie.
Variables
En Scala, une nouvelle variable «i» de type entier peut être initialisée à la valeur 42 de la façon suivante
scala> val i : Int = 42 i: Int = 42
Le langage Scala propose deux catégories de variable : les variables dont la valeur peut changer et celles dont la valeur ne peut changer. La deuxième catégorie est comparable aux variables déclarées avec le mot-clé «final» en Java. Cette catégorie de variable est porposée par Scala pour permettre aux développeurs de faire de la programmation fonctionnelle avec des données non-mutables. Une variable mutable peut être déclarée avec le mot-clé «var», alors qu'une variable non-mutable est déclarée avec le mot-clé «val». L'exemple suivant met en évidence cette différence :
scala> val i1 : Int = 42 i1: Int = 42 scala> i1 = 43 <console>:8: error: reassignment to val i1 = 43 ^ scala> var i2 : Int = 42 i2: Int = 42 scala> i2 = 43 i2: Int = 43
Ci-dessus, la variable i1 est déclarée comme une variable non-mutable; sa réassignation est donc impossible et l'interpréteur retourne une erreur. Au contraire, la variable i2 est mutable et l'interpréteur accepte qu'une nouvelle valeur lui soit assignée.
Contrairement aux langages C et Java, le type d'une variable est spécifié après son nom lors de sa déclaration. Le nom de la variable et son type doivent être séparés par le caractères «deux points». Cependant, il est tout à fait possible d'omettre le type si celui-ci est évident. L'exemple suivant montre un cas où le développeur laisse le langage deviner le type, puis un deuxième cas où il force l'utilisation d'un Float.
scala> val i = 42 i: Int = 42 scala> val f : Float = 42 f: Float = 42.0
Fonctions
En Scala, les fonctions s'écrivent de la façon suivante :
def square(value : Int) : Int = { return value * value }
Cette notation comporte :
- Le mot-clé «def» qui permet de préciser qu'il s'agit de la déclaration d'une fonction
- Le nom de la fonction, ici «square»
- Les paramètres de la fonction, chacun des paramètres doit être accompagné de son type (entier, booléen, etc)
- Le type de retour de la fonction qui doit être précédé du caractère «deux-points» et suivi du caractère «égal»
- Le corps de la fonction entre accolades. Comme dans les autres langages, le mot-clé «return» termine la fonction et retourne la valeur spécifiée
Jusqu'ici, il n'y a rien de surprennant pour un programmeur habitué à des langages comme le C ou le Java. Cependant, cette même fonction peut s'écrire de la façon suivante, qui peut paraître un peu plus déroutante :
def square(value : Int) = value * value
Tout d'abord, le type de retour n'a pas besoin d'être précisé et sera déduit lors de la compilation. Ensuite, les accolades sont elles-aussi facultatives quand le corps de la fonction ne contient qu'une opération. Enfin, le mot-clé return n'est lui aussi pas indispensable : en effet, la dernière valeur utilisée dans la fonction sera celle renvoyée, même si c'est une valeur retournée par une fonction
Les classes
Scala propose le paradigme de programmation orienté objet. Ainsi, les programmes Scala sont organisés en classes. La définition de ces classes est très proche de Java. Les méthodes de classes s'écrivent de la même manière que les fonctions présentées précédemment.
L'exemple suivant défini une classe représentant un chat qui peut être tué au travers de sa méthode kill() jusqu'à ce qu'il ait épuisé ses sept vies. La méthode isDead() de cette classe renvoie vrai lorsque le nombre de vie restante est égal à zéro.
class Cat(private val name : String) { private var numberOfLives : Int = 7 def getName() = name def kill() { if(numberOfLives >= 0) numberOfLives -= 1 } def isDead() = numberOfLives == 0 }
Certains détails du code ci-dessus ont besoin d'être expliqués :
- Le mot-clé «class» permet de commencer la définition d'une classe.
- En Scala, une classe peut prendre des paramètres. Ces paramètres se trouvent entre parenthèses à droite après le nom de la classe. Ces paramètres vont devenir des attributs de la classe. Dans l'exemple ci-dessus, la classe «Cat» va donc devoir être instancié en passant en paramètre le nom du chat.
- Comme en Java, les mots-clés private, protected et public permettent de changer la visibilité d'une classe, d'un attribut ou d'une méthode d'une classe.
Comme en Java, les classes sont instanciées grâce au mot-clé «new» suivi du nom de la classe et enfin, de ses paramètres.
«Singleton objects» et «Companion object»
Le langage Scala ne permet pas de définir des méthodes ou des attributs statiques dans une classe. Pour faire cela, il faut utiliser un type de classe particulier qui est instancié par défaut et ne peut être réinstancié. Scala utilise le nom de «singleton object» pour définir ce type de classe. La définition d'un singleton object est similaire à la définiton d'une classe, sauf qu'il faut utiliser le mot-clé «object» à la place du mot-clé «class».
C'est typiquement dans un singleton object que va se trouver la méthode main, c'est-à-dire le point d'entrée du programme. Le code suivant montre la définition d'un singleton object contenant une méthode main utilisant les classe Cat définie précédemment :
object test { def main(argv : Array[String]) { var myCat = new Cat("Pima") while(!myCat.isDead) { println(myCat.getName + " is not dead") myCat.kill() } println(myCat.getName + " is dead :'-(") } }
Dans le code précédent, la classe Cat est instanciée en donnant le nom «Pima» au chat. Celui-ci est ensuite tué jusqu'à ce qu'il ait épuisé ses sept vies.
Les singleton objects ne résolvent que la moitié du problème : parfois, certains attributs ou méthodes statiques appartiennent vraiment à une classe et non pas de raison d'être séparés de celle-ci. Pour cela, Scala propose les «companion objects», qui sont des singleton objects ayant le même nom qu'une classe. Les companion objects d'une classe ont une visibilité totale sur sa classe, et inversement. Pour des raisons de lisibilité, une classe et son companion object sont écrits dans le même fichier (ce qui est tout à fait autorisé en Scala).
Les boucles
Sans surprise, le langage Scala propose la structure de contrôle if/else pour faire des tests simples que n'importe quel programmeur n'aura aucun mal à utiliser :
if(i == 0) println("i equals zero") else println("i does not equal to zero")
Le fait que le Scala soit un langage impératif ET fonctionnel nécessite la présence de deux types de boucles : les boucles sur des objets mutables et les boucles sur des objets non-mutables. Les deux prochaines sous-sections s'attachent à présenter la même boucle dans les deux paradigmes
La boucle while
Le langage Scala ne propose pas la boucle for dans sa forme la plus courante codée ci-dessous en C :
/* Ce n'est pas du Scala ! */ int i; for(i=0 ; i<10 ; i++) printf("%d\n", i);
En Scala, il faut utiliser la boucle while. L'exemple suivant, écrit en Scala cette fois-ci fait la même chose que précédemment :
var i : Int = 0 while(i < 10) { println(i) i = i+1 }
Plus long et moins lisible n'est-ce pas ? En réalité, le choix de ne pas proposer cette forme de boucle for a été fait en Scala pour privilégier sa forme fonctionnelle, décrite dans la sous-section suivante.
Il est important de noter que la boucle while est une boucle purement impérative, puisqu'elle travaille sur le changement de valeur d'une variable. En programmation fonctionelle, la valeur d'une variable ne peut changer, la boucle while n'a donc plus aucune utilité !
La boucle for
En Scala, la boucle for utilise la syntaxe suivante (l'exemple est le même que précédemment) :
for(i <- 0 to 9) println(i)
Voici les détails du morceau de code ci-dessus :
- La boucle for prend en paramètre un générateur. Un générateur est une classe qui «génère» la valeur suivante lorsqu'elle lui est demandée. Il est impossible de remettre un générateur à zéro, aussi un générateur est à «usage unique».
- La variable non-mutable i permet d'exploiter la valeur suivante du générateur dans le corps de la boucle. Cette variable est bien non-mutable, bien que sa valeur change à chaque itération. Ainsi, sa valeur ne pourra donc pas être modifiée dans le corps de la boucle
- La flèche <- permet de créer le générateur lui-même en utilisant le tableau qui se trouve à sa droite.
- La partie «0 to 9» permet de créer un tableau initialisé avec des valeur de 0 à 9. C'est les valeurs de ce tableau qui vont être utilisées par le générateur.
Manipulation des listes
Les listes sont un type de variable très utilisé en Scala, et plus généralement en programmation fonctionnelle. Cette section a pour objectif d'expliquer brievement les principaux outils proposés par Scala pour les manipuler.
En Scala, la création d'une nouvelle liste se fait au travers d'une factory présente dans le companion object de la classe List. Le mot-clé «new» ne doit donc pas être présent lors de l'instanciation d'une nouvelle liste, bien que cela puisse paraître étrange au premier abord. Puisque les listes sont non-mutables en Scala, la liste vide est instanciée systèmatiquement et sa référence sera renvoyée pour toute instanciation de liste vide dans le programme. Cette instance de liste vide a un nom : «Nil». L'exemple suivant applique ces notions d'instanciation de listes :
scala> val l1 : List[Int] = List() l1: List[Int] = List() scala> val l2 : List[Int] = Nil l2: List[Int] = List() scala> val l3 = List(42, 666) l3: List[Int] = List(42, 666)
Le langage Scala propose les deux mots-clés «::» et «:::» pour manipuler les listes :
- Le mot-clé «::» permet de concaténer un élément à la fin d'une liste. Ce mot-clé porte le nom de «cons».
- Le mot-clé «:::» permet quant à lui de concaténer une liste à la fin d'une autre.
Il est important de rappeler que les listes sont non-mutables, ainsi ces deux opérateurs renvoient systématiquent une nouvelle liste sur laquelle les changements ont été appliqués. Voici un exemple d'utilisation de ces deux opérateurs : une liste vide se voit d'abord concaténer avec un élément, puis avec une liste :
scala> val l1 = Nil l1: scala.collection.immutable.Nil.type = List() scala> val l2 = 1 :: l1 l2: List[Int] = List(1) scala> val l3 = l2 ::: List(2, 3, 4) l3: List[Int] = List(1, 2, 3, 4)
Conclusion
Scala est bien un langage objet-fonctionnel-impératif :
- L'architecture des programmes écrits en Scala repose sur des classes
- Scala fourni tout une suite d'outils pour faire de la programmation fonctionnelle, comme la boucle for
- Scala autorise tout de même la programmation impérative pour ne pas dérouter les nouveaux venus : variables mutables et boucle while