[update] cet article a été mis à jour suite aux retours d’Alexandre Bertails
Après un premier article qui vous a fait pas mal réagir, je vais vous parler des Traits et des Collections. J’ajoute un peu plus de programmation fonctionnelle dans cet épisode.
Les Traits
Les Traits en Scala sont un nouveau concept qui demande un peu de temps à un développeur Java du Canal Historique comme moi. Regardons les limitations des interfaces en Java : une classe peut implémenter un certain nombre d’interfaces. Or une interface ne nous permet pas de définir du code commun justement pour l’ensemble des classes qui implémentent celle-ci. Au lieu de cela, il faut mettre en place des stratégies et des patterns pour regrouper des ensembles fonctionnels. Bref nous nous en sortons, avec un peu de gymnastique. A ce propos, lorsque je vois l’utilisation des annotations en Java sur certains projets, j’ai le sentiment de voir des cas d’usages similaires aux Traits en Scala, que nous avons comblé avec des annotations.
Les Traits en Scala sont empilables, ce qui permet d’ajouter de multiples caractéristiques définies dans différents Traits. Contrairement à une interface Java, les Traits peuvent implémenter des méthodes. Il ne sera alors pas nécessaire de les redéfinir dans les classes qui étendent ces Traits.
J’aime l’idée qu’une Interface est un espace de nom qui permet de regrouper par caractéristiques des objets (Serializable, Comparable). Cela me fait penser que les Traits sont similaires à des adjectifs que l’on ajoute à un objet. . Attention, il ne s’agit pas d’héritage multiple comme en C++, mais de la possibilité d’étendre des interfaces, en définissant un ordre. La notion de Trait est inspiré des Mix-In du langage Ruby.
Les Traits sont donc comme des interfaces avec une implémentation partielle, et ils permettent d’empiler dans un certain ordre un ensemble de fonctionnalités à une Class. Nous verrons avec les Collections que c’est très pratique.
Prenons par exemple un panier sur un site Web. Ce panier est capable d’accumuler des articles. Nous allons voir comment il est possible d’enrichir les possibilités de notre objet initial en y ajoutant des traits, sans cependant faire de l’héritage multiple.
Je commence par définir une classe abstraite pour définir la notion de Scanner de prix. Un Scanner est capable d’additionner le prix de chaque article qui passe, et ensuite de vous donner le total à payer
abstract class PriceScanner { def scanArticle(x: Double) def getTotalPrice(): Double }
Définissons ensuite un simple Scanner
class ClassicPriceScanner extends PriceScanner { private var total:Double = 0.0 def scanArticle(x: Double) = { total+=x } def getTotalPrice() = { total } }
Je vais vous montrer qu’il est possible d’empiler les Traits pour ajouter de manière sélective quelques opérations. Nous allons définir un Trait pour calculer la TVA lorsqu’un article est scanné, et un Trait pour convertir en US Dollar le prix final :
// Scala // Compute the price with Value Added Taxes trait FrenchTaxIncluded extends PriceScanner { // Add the French V.A.T. abstract override def scanArticle(x: Double) { super.scanArticle(x * 0.196 + x)} }
Notez que le Trait étend la classe de base
/** * Convert to US Dollar the total amount. */ trait InUSDollar extends PriceScanner { abstract override def getTotalPrice() = { super.getTotalPrice()*1.3556 } }
Les Traits sont réellement des caractéristiques que nous allons ajouter à la création de nos instances, au lieu de devoir les ajouter à la définition de notre classe. C’est maintenant que cela devient intéressant. Dans l’exemple ci-dessous j’ai déclaré 3 scanners différents. Chacun de ces Scanners dispose de plus ou moins de fonctionnalités selon les Traits. Notez aussi l’usage du mot clé « with » qui permet de donner l’ordre dans lequel les Traits seront ajoutés à la classe initiale :
object Test { def main(args: Array[String]) = { val myPanier = new ClassicPriceScanner() with FrenchTaxIncluded myPanier.scanArticle(100.0) val myUSDPanier : ClassicPriceScanner = new ClassicPriceScanner() with InUSDollar myUSDPanier.scanArticle(100.0) val myUSDPanierAndVAT : ClassicPriceScanner = new ClassicPriceScanner() with InUSDollar with FrenchTaxIncluded myUSDPanierAndVAT.scanArticle(100.0) println("Total in EUR: "+myPanier.getTotalPrice()) println("Total in USD: "+myUSDPanier.getTotalPrice()) println("Total in USD with VAT: "+myUSDPanierAndVAT.getTotalPrice()) } }
L’exécution de ce programme donne alors le résultat suivant :
Total in EUR: 119.6 Total in USD: 135.56 Total in USD with VAT: 162.12975999999998
Notez que ces tests ne sont pas très élégants. Pourquoi ne pas utiliser dès maintenant la librairie Specs de Scala Tools afin d’écrire des tests orientés développements ? La pratique du Behavior-Driven Development dont je pourrai parler dans un autre article, consiste à décrire sous forme de spécifications exécutables le résultat attendu.
Télécharger specs-1.4.3.jar et ajoutez-le dans votre Classpath pour pouvoir lancer cette spécification :
package org.letouilleur.tutorial.test /** * BDD Specification for Traits article. * @author Nicolas Martignole */ import org.specs._ object PriceScannerSpec extends Specification { "ClassicPriceScanner with 2 articles at 100.0 EUR each " should { "return a total of 200.0 " in { val cp = new ClassicPriceScanner() cp.scanArticle(100.0) cp.scanArticle(100.0) cp.getTotalPrice() mustEqual 200.0 } } "ClassicPriceScanner with 2 articles at 100.0 EUR each decorated with the FrenchTaxIncluded Trait" should { "return a total of 238.2 EUR" in { val cp = new ClassicPriceScanner() with FrenchTaxIncluded cp.scanArticle(100.0) cp.scanArticle(100.0) cp.getTotalPrice() mustEqual 239.2 } } }
Lorsque l’on exécute cette spécification, voici le résultat :
Specification "PriceScannerSpec" ClassicPriceScanner with 2 articles at 100.0 EUR each should + return a total of 200.0 Total for SUT "ClassicPriceScanner with 2 articles at 100.0 EUR each ": Finished in 0 second, 0 ms 1 example, 1 expectation, 0 failure, 0 error ClassicPriceScanner with 2 articles at 100.0 EUR each decorated with the FrenchTaxIncluded Trait should + return a total of 238.2 EUR Total for SUT "ClassicPriceScanner with 2 articles at 100.0 EUR each decorated with the FrenchTaxIncluded Trait": Finished in 0 second, 0 ms 1 example, 1 expectation, 0 failure, 0 error Total for specification "PriceScannerSpec": Finished in 0 second, 44 ms 2 examples, 2 expectations, 0 failure, 0 error
Sympa non ? Au passage vous noterez que Scala est particulièrement à l’aise dans le domaine des DSL.
Je m’arrête là pour les Traits. Je sais que les Aficionados de Scala vont largement commenter mes petits tests, qui ne doivent pas casser 2 pattes à un canard. Je débute les gars, donc cool, respirez un coup et pensez aux lecteurs qui n’y connaissent rien. Soyez pédagogue.
Les Collections
Deuxième sujet aujourd’hui dont je veux vous parler : les Collections. Nous ouvrons la boîte de la programmation fonctionnelle dans cette partie. Je ne vais pas vous expliquer ce qu’est une Map ou un Set. En Scala nous retrouvons des objets List, Set et Map. Il existe 2 familles de collections : les objets immuables et les objets non immuables. Scala encourage l’utilisation d’objets immuables, ce qui rend service lorsque nous ferrons de la programmation concurrentielle la prochaine fois. En effet, un objet qui ne peut pas être modifié, n’a pas besoin d’être synchronisé.
Les Collections en Scala sont disponibles en plusieurs versions. Prenons les Lists par exemple, Scala 2.8 propose :
– scala.collection.immutable.List
– scala.collection.mutable.LinkedList
(notez qu’il n’existe pas de scala.collection.mutable.List)
– scala.collection.jcl.LinkedList
Le 3ème type jcl est un wrapper de la Class java.util.LinkedList.
Une Map est simple à écrire, c’est même plus concis que la version Java :
val aMap = Map("Nicolas"->180,"Pierre"->172,"Jacques"->181) println(aMap.size) // affiche 3
Nous pouvons commencer à utiliser quelques spécificités de Scala pour créer une deuxième Map de type String,String où la taille de chaque personne est convertie en mètre, et ensuite transformé en chaine de caractère :
object MapSample { def main(args: Array[String]) = { val aMap = Map("Nicolas"->180,"Pierre"->172,"Jacques"->181) println(aMap.size) val convertedInMeter = aMap map {kv => (kv._1,kv._2*0.01+"m")} println("People size in meter : "+convertedInMeter) } }
L’exécution du code ci-dessus donne :
3 People size in meter : ArrayBuffer((Nicolas,1.8 m), (Pierre,1.72 m), (Jacques,1.81 m))
Nous pouvons aussi par exemple filtrer les clés ou les valeurs de la map, afin de créer une deuxième map ne contenant que les personnes qui font 1.80 mètre ou plus :
// Personne qui font plus de 180cm val tallPeople = aMap filter {kv => kv._2 > 180 } println("People taller than 180cm "+tallPeople) // Execution -> seul Jacques dépasse les 180 cm People taller than 180cm Map(Jacques -> 181)
Il existe un grand nombre de fonctions que l’on peut utiliser sur les collections :
– def drop (n: Int) : Collection[A] permet de retirer les n premiers éléments d’une itération, ou retourne vide sinon
– def dropWhile( p : (A) => Boolean) : Collection[A] permet de supprimer d’une collection ses éléments tant que la condition de test retourne true.
– def exists(p: (A) => Boolean) : Iterable[A] permet de vérifier qu’un prédicat p se vérifie sur l’un des éléments de la collection
– def filter(p: (A) => Boolean) : Iterable[A] permet de filtrer la collection comme vu dans l’exemple ci-dessus
– def find(p: (A) => Boolean) : Option[A] permet de trouver le premier élément dans l’iterable qui satisfait le prédicat p
– def findIndexOf(p: (A) => Boolean) : Int permet de trouver le premier emplacement de l’élément qui satisfait le prédicat p, sinon retourne -1
– def forAll(p: (A) => Boolean) : Boolean permet d’appliquer un prédicat p à chacun des éléments de l’iterable et retournera true si le prédicat a retourné true pour chacun des éléments. C’est très pratique pour itérer une collection et effectuer un traitement à la volée.
– def indexOf [ B > A](elem : B) : Int retourne la position du premier élément qui correspond à l’objet spécifié
– def partition(p : (A) => Boolean) : (Iterable[A], Iterable[A]) permet de découper cette Iterable en 2 Iterables selon le prédicat spécifié
– def sameElements [B >: A](that : Iterable[B]) : Boolean permet de vérifier si un autre Iterable contient les éléments du premier Iterable
– def take(n : Int) : Collection[A] permet de prendre uniquement les n premiers éléments d’un Iterable, ou sinon retourne l’objet lui-même s’il est trop petit
– def takeWhile(p : (A) => Boolean) : Iterable[A] enfin permet de parcourir l’Iterable et d’accumuler dans une nouvelle Iterable les éléments tant que le prédicat est vrai.
Il y a d’autres méthodes pour les Set et pour les Maps. Nous constatons qu’il existe un ensemble de fonctions prêtes à l’emploi.
Folding et Reducing
L’opérateur foldLeft permet de parcourir une List et de retourner soit une collection plus petite, soit parfois une seule valeur. Prenons le cas le plus simple (mais pas le plus représentatif) : vous souhaitez calculer la somme d’une liste d’entier. Je créé une liste de 1 à 10 puis j’applique la fonction foldLeft :
object FoldingSample { def main(args: Array[String]) = { val shortList = 1 to 10 toList val result=shortList.foldLeft(0)((b,a) => b+a ) println(result) // will output 55 } }
foldLeft comme foldRight est ce que l’on appelle une « curried function ». Cette fonction prend 2 paramères (z et f) dans 2 ensembles de parenthèses. Ici z vaut 0 et f est une fonction d’addition. La définition Scala de la fonction est la suivante :
def foldLeft[B](z: B)(f: (B, A) => B): B
z est de type B, n’importe quel type. f est une fonction qui prend B et A et retourne une valeur de type B. A sera en fait chacun des éléments de la collection sur laquelle nous appliquons la fonction foldLeft. Donc ici nous prenons un objet de type B, en l’occurrence 0 qui est un Int, nous effectuons l’addition de la valeur courante de la liste avec la valeur précédemment stockée, et nous retournons l’ensemble
Ici pour le premier élément de la liste, la fonction additionne 0 et 1, ce qui donne 1. Ensuite nous évaluons le deuxième élément de la liste qui est 2. Nous ajoutons l’ancien résultat 1 à 2, ce qui donne 3. Nous avançons au 3ème élément de la liste qui est 3. Nous évaluons la fonction avec l’ancienne valeur de 3, et cela donne 6. Peut-être que le détail vous aidera à comprendre ce qui se passe:
(0+1)=1
(1+2)=3
(3+3)=6
(6+4)=10
(10+5)=15
(15+6)=21
(21+7)=28
(28+8)=36
(36+9)=45
(45+10)=55
Cela vous semble peut-être un peu abstrait, mais c’est très pratique. Voici quelques exemples d’utilisation de cette fonction :
// Moyenne de la série val shortList = 1 to 10 toList def average(list: List[Int]): Double = list.foldLeft(0.0)(_+_) / list.foldLeft(0.0)((r,c) => r+1) println(average(shortList)) // affiche 5.5 // Produit : notez que l'on doit partir de 1 et pas de 0 def product(list: List[Int]): Int = list.foldLeft(1)(_*_) println(product(shortList)) // affiche 3628800
Conclusion
Scala propose donc des nouveautés et des concepts avancés comme les Traits, qui permettent d’introduire une programmation orientée fonctionnelle sans soucis. Nous n’avons pas parlé d’une dizaine de sujets, et je m’excuse auprès des 7 lecteurs qui maitrisent Scala et qui lisent le blog. Ecrire ces articles demande beaucoup de travail, entre 4 et 6 heures à chaque fois. Nous parlerons un peu de la concurrence et de la beauté de Scala dès lors qu’il s’agit de programmation multi-thread.
Encore une fois un super article. J’ai découvert scala avec ton premier post et j’en suis déjà fan. Un langage qui permet de mêler objet et fonctionnel tournant sur une jvm avec les api du jdk… Ouch ça claque !
J’attends impatiemment le prochain post 😉
Bonne continuation !
[Note de Nicolas Martignole : j’ai modifié le commentaire d’Alexandre après avoir repris une partie de mon article. Merci Alexandre pour ton retour sur la partie des Listes, qui était complétement faux dans mon premier article.]
[…] commentaires supprimés […]
> foldLeft comme foldRight est ce que l’on appelle une « curried function ». Cette fonction prend 2 paramères (z et f)
foldLeft ne prend pas deux paramètres. Elle prend *un* paramètre ‘z’ et renvoie une fonction qui prend *un* paramètre ‘f’ et dans laquelle la variable ‘z’ est potentiellement liée. (cf. http://en.wikipedia.org/wiki/Free_variables_and_bound_variables).
C’est toute la différence entre ces deux types :
* version currifiée : foldLeft[B] : B -> ((B, A) -> B) -> B
* version non currifiée : foldLeft[B] : (B, ((B, A) -> B)) -> B
> Scala propose donc des nouveautés et des concepts avancés comme les Traits, qui permettent d’introduire une programmation orientée fonctionnelle sans soucis
J’avoue ne pas comprendre le rapport entre « Traits » et « programmation orientée fonctionnelle ». C’est plutôt une nouvelle sémantique pour les langages orienté objet qui règle élégamment le problème de l’héritage multiple.
> je m’excuse auprès des 7 lecteurs qui maitrisent Scala et qui lisent le blog. Ecrire ces articles demande beaucoup de travail, entre 4 et 6 heures à chaque fois
Et je te remercie de prendre ce temps pour faire connaître ce langage. Encore une fois, je serai heureux de te relire avant publication si tu le souhaites.
Et je suis désolé si mes commentaires font empêcheur de tourner en rond mais je trouve particulièrement important de donner ces précisions car ces erreurs causent beaucoup trop de tort. C’est ta faute : tu attires trop de lecteurs sur ton blog 🙂 et il nous faut beaucoup trop d’efforts ensuite pour corriger cela…
@Alexandre je te ferai relire le 3eme qui n’est pas encore terminé. Un grand merci pour ton retour. J’ai repris un peu mon article afin de corriger tout de suite les erreurs.