Le langage Scala

Concepts avancés

L'objectif de cette dernière partie est de présenter un maximum de concepts de programmation proposés par le langage Scala. Tout ces concepts sont bien sûr assortis d'exemples.

Appel avancé de méthodes

Le langage Scala autorise deux syntaxes pour l'appel d'une méthode. L'exemple suivant présente ces deux syntaxes en utilisant sur une liste préalablement initialisée la méthode contains. La méthode contains renvoie le booléen «true» si sa valeur passée en paramètre est présente dans la liste, «false» sinon.

scala> val l = List(0, 1, 2)
l: List[Int] = List(0, 1, 2)

scala> l.contains(2)
res4: Boolean = true

scala> l contains 2
res5: Boolean = true

Dans l'exemple précédent, la syntaxe du premier appel de méthode est le même qu'en Java, et est pour cette raison simple à comprendre. La deuxième syntaxe se compose de trois éléments séparés par un espace :

Cette syntaxe d'appel de méthode peut paraître sans intérêt, mais elle offre en fait une très grand flexibilté dans le langage Scala. En effet, tous les opérateurs du langage Scala sont en fait des fonctions. Lorsque l'opération «1 + 4» est passée au compilateur, celui-ci va instancier la classe Int pour les valeurs 1 et 4, puis il va appeler la méthode «+» sur l'instance de Int de valeur 1 en lui passant pour paramètre le Int de valeur 4. Dans une notation plus classique, cela donne «(1).+(4)» (les parenthèses autour du 1 sont nécessaire pour que le compilateur ne le prennent pas pour un double...).

Cette solution permet d'une part de définir de nouveaux opérateurs dans le langage Scala, et d'autre part de «redéfinir les opérateurs existant». Bien entendu, le terme «redéfinir des opérateurs» n'a aucun sens en Scala : les opérateurs habituels comme le plus ou la division sont juste définis dans tous les types de base. Pour illustrer ce propos, le code la classe «Complex» représentant un nombre complexe est donné ci-dessous :

class Complex(private var re : Int, private var im : Int) {
	def +(c : Complex) = new Complex(re + c.re, im + c.im)
	
	def tangentIsGreater(c : Complex) = math.sqrt(re*re + im*im) > math.sqrt(c.re*c.re + c.im*c.im)

	override def toString = re + "+" + im + "i"
}

Trois méthodes sont définies dans la classe ci-dessus :

La méthode main utilisée pour exploiter cette classe est la suivante :

object TestComplex {
	def main(argv : Array[String]) {
		var c = new Complex(10, 20)
		c = c + new Complex(1, 2)
		println(c) // Affiche «11+22i»
		println(c tangentIsGreater new Complex(25, 25)) // affiche «false»
	}
}

L'exécution de ce programme affiche une première ligne contenant «11+22i», ce qui correspond bien à la somme des nombres complexes 1+2i + 10+20i. La deuxième ligne affiché est «false». En effet, la tangeante de 25+25i est bien supérieure à la tangeante de 11+22i.

Ce concept du «tout est méthode» permettant au développeur d'écrire ses propres opérateurs est typiquement une des choses qui rend le langage Scala «scalable», c'est-à-dire extensible en fonction des besoins du développeur.

Pattern matching et récursivité

Le fait que les variables soient non-mutables en programmation fonctionnelle peut poser problème pour les programmeurs habitués à la programmation impérative. Cette section a pour objectif de présenter deux outils permettant de résoudre un grand nombre de problème en programmation fonctionnelle : le «pattern matching» et la récursivité.

Pattern matching

En Scala, le mot-clé «match» est l'équivalent du mot-clé «switch» dans les langages de programmation comme le C ou le Java. Cependant, son fonctionnement à été étendu à la reconnaissance de motif dans les données comparée — d'où le terme «pattern matching». L'exemple suivant permet d'effectuer trois actions distinctes selon que la liste est vide, n'a qu'un élément ou en a plusieurs :

l match {
	case Nil => println("La liste est vide")
	case x::Nil => println("La liste ne contient qu'un seul élément")
	case x::xs => println("La liste a au moins deux éléments")
}

Voici les détails de chaque «case» du code ci-dessus :

Dans le code ci-dessus, le choix des noms x et xs n'a rien d'obligatoire et ne fixe en aucun cas leur type : le compilateur Scala considère simplement qu'une variable placée à gauche de l'opérateur «::» est un élément et qu'une variable placée à droite est une liste.

Récursivité

Certaines opérations qui paraissent basiques en programmation impérative peuvent s'avérer dur à implémenter pour un développeur souhaitant passer de la programmation impérative à la programmation fonctionnelle. Un bon exemple de ce problème est l'inversion de tous les éléments d'une liste. En programmation impérative, l'équivalent d'une liste est un tableau. La fonction suivante, écrite en C, permet donc d'inverser les éléments d'un tableau :

/* Fonction écrite en C, pas en Scala... */
void reverse(int a[], int size) {
	int i, tmp;
	for(i=0 ; i<size/2 ; i++) {
		swap(&a[i], &a[size-1-i]);
	}
}

La fonction ci-dessus détruit ses paramètres. En production, il serait préférable de renvoyer une nouvelle liste. Ce choix a été fait ainsi pour souligner la différence entre un code purement impératif et un code purement fonctionnelle.

En Scala, deux outils vont être utilisés pour résoudre ce problème : le «pattern matching» et la récursivité. L'exemple suivant présente une solution qui utilise ces deux outils :

scala> def reverse(l : List[Int]) : List[Int] = {
     | l match {
     | case List() => List()
     | case x::xs => reverse(xs):::List(x)
     | }}
reverse: (l: List[Int])List[Int]
scala> l
res36: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> reverse(l)
res37: List[Int] = List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
scala> l
res38: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

La fonction reverse cherche deux motifs dans la liste qui lui est passée : soit la liste vide, auquel cas elle n'a rien a faire et renvoie une liste vide, soit la liste contient au mois un élément. Dans ce deuxième cas, la fonction enlève la tête de la liste et s'appelle récursivement en passant en paramètre la liste privée de son premier élément. Lorsque l'appel récursif est terminé, la fonction concatène la liste renvoyée à une liste contenant seulement la tête précédemment enlevée et retourne le résultat.

Dans l'exemple ci-dessus, la foncton reverse est testée avec une liste contenant les valeurs de 1 à 10. Il est important de remarquer qu'après l'appel à reverse sur la liste l, la liste l n'a pas changée : la liste renvoyée par reverse est bien une nouvelle liste.

Yield

Le dernier outil présenté ici sera le mot-clé «yield», permettant de créer une nouvelle collection à partir d'une boucle. Le mot-clé yield s'utilise un peu comme le mot-clé «return», sauf qu'il ne termine pas la fonction en cours mais ajoute la valeur à une collection. Lorsque la fonction est effectivement terminée, la collection contenant tous les éléments ajoutés par yield sera retournée. L'exemple suivant permet d'extraire les nombres impairs entre 0 et 10 en utilisant yield :

scala> for{i <- 1 to 10 if i%2 != 0} yield i
res13: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 3, 5, 7, 9)

Le résultat est du type Vector, Vector étant une classe permettant l'accès en temps constant aux élément qu'il contient, contrairement aux listes.

Conclusion

Tout d'abord, le langage Scala est bien un langage scalable : le fait que les opérateurs soient des fonctions permet une grande flexibilité pour le programmeur qui peut choisir de les implémenter ou non dans ses classes. Ensuite, Scala propose tous les outils nécessaires pour l'utilisation efficace de la programmation fonctionnelle : le pattern matching et le mot-clé «yield» en sont deux bons exemples.