Note importante : au moment où j’écris ces lignes, je découvre Scala. Cet article n’est pas destiné à être une référence. Il ne prétend pas être exhaustif, exact et parfait. Lisez en fin d’article les commentaires qui complètent et corrigent certains points incorrects. Merci à Alexandre encore une fois pour son retour.
Après un article d’introduction sur Scala puis un autre sur les Traits et les Collections, nous allons parler un peu de Pattern Matching.
Prenons un exemple en Java pour comprendre. Je dois retourner une image selon la catégorie d’une annonce. Si l’annonce est de type CDI, je souhaite retourner un logo CDI.png. Si l’annonce est de type CDD, un autre logo, etc. En Java il n’est pas possible d’utiliser des String dans des boucles switch/case. Voici la version Java:
public static String getURLForJobType(String type) { if (type == null || type.trim().equals("")) return null; if (type.equalsIgnoreCase("cdi")) { return "/public/images/icons/cdi.png"; } if (type.equalsIgnoreCase("cdd")) { return "/public/images/icons/cdd.png"; } if (type.equalsIgnoreCase("stage")) { return "/public/images/icons/stage.png"; } if (type.equalsIgnoreCase("apprentissage")) { return "/public/images/icons/cpp.png"; } if (type.equalsIgnoreCase("autre")) { return "/public/images/icons/other.png"; } return null; }
Scala comme en C# est capable de matcher une expression quelque soit son type. Cela permet de reprendre le code ci-dessus, et d’écrire :
object TestPattern { def main(args: Array[String]) { println("Test Scala: " + getURLForJobType("CDI")) } def getURLForJobType(typeContrat: String): String = typeContrat.toLowerCase match { case "cdi" => "/public/images/cdi.png" case "stage" => "/public/images/icons/stage.png" case "contrat d'apprentissage" | "contrat de professionalisation" => "/public/images/icons/cpp.png" case "autre" => "/public/images/icons/other.png" case _ => "/public/images/noimages.png" } }
La version Scala permet de matcher sur des chaînes de caractères et de traiter les cas multipe avec l’opérateur « | ».
En Scala, tout est une instance d’une classe (sauf les méthodes) ([update : voir le commentaire d’Alexandre plus bas). Les primitives Java comme int sont traitées comme des instances par le compilateur Scala. C’est une opération qui s’effectue lors de la compilation et de la génération du bytecode Java. Celui-ci est d’ailleurs optimisé. Donc l’addition de 2 Ints en Scala donne exactement le même bytecode que l’addition de 2 int en Java. Et donc, il n’y a pas de dégradation des performances, Scala reste aussi efficace que Java. Lorsque le compilateur Scala doit par exemple stocker un Int dans une Map, celui-ci stocke la primitive dans une boîte.
En Scala, les primitives sont donc représentées par les classes Int, Long, Double, Float, Boolean, Char, Byte et Short. Toutes ces classes dérivent de la classe Any. Celle-ci est dérivée en AnyRef et AnyVal.
Scala a aussi une représentation de void appelée Unit. Cela vous permet donc de retourner explicitement l’équivalent de void dans les traitements.
Unit se note simplement avec une parenthèse ouvrante/fermante : ()
Très pratique, il est aussi possible d’utiliser plusieurs types dans la même expression :
object TestPattern { def main(args: Array[String]) { println("Test with an Int: " + getURLForJobType2(555)) println("Test with a Float : " + getURLForJobType2(10.33)) } def getURLForJobType2(typeContrat: Any) = typeContrat match { case "cdi" => "/public/images/cdi.png" case "stage" => "/public/images/icons/stage.png" case 555 => "matched an Int" case 10.33 => "matched a Float" case _ => None } }
L’argument « typeContrat » est maintenant de type Any. L’underscore est équivalent à default en Java et permet de matcher tout.
Options, Some et None : éviter les nulls
Comment pourrions-nous maintenant aller plus loin et traiter le cas où typeContrat serait null ?
La classe Option et ses 2 classes dérivées Some et None sont très simples à utiliser.
En Java, nous sommes habitués à utiliser null lorsque nous souhaitons affecter ou vérifier la valeur d’une référence. En Scala, tout est objet comme dit plus haut. C’est donc logiquement que Scala vous encourage à utiliser le type Option pour les variables et les retours de fonctions. Lorsqu’il n’y a aucunes valeurs, utilisez None. Lorsqu’il y a une valeur, vous pouvez utiliser Some pour wrapper la valeur.
Modifions notre méthode afin qu’elle retourne le type Option dérivé soit en None, soit en Some.
object TestPattern { def main(args: Array[String]) { println("Test with a String : " + getURLForJobType("CDI").get ) println("Test with an Int : " + getURLForJobType(3).get ) println("Test with a bad value : " + getURLForJobType("Bad Entry").getOrElse("Not a valid value")) } def getURLForJobType(typeContrat: Any) : Option[Any] = typeContrat match { case "CDI" => Some("/public/images/" + typeContrat + ".png") case 3 => Some("/public/images/" + typeContrat + "_icon.png") case _ => None } } // Execution Test with a String : /public/images/CDI.png Test with an Int : /public/images/3_icon.png Test with a bad value : Not a valid value
Notez à la ligne 4 l’utilisation de l’opérateur get qui retourne la valeur d’une option. A la ligne 10 j’ai aussi modifié la signature de ma méthode afin de spécifier que je retourne un objet Option. A la ligne 6 la fonction getOrElse permet de s’assurer qu’une valeur sera utilisée même si la fonction appelée venait à retourner None. En règle général, en Scala la vérification à priori des arguments d’une méthode devient inutile. Nous verrons que le if/then/else tend à être remplacé par des « cases », ce qui est plus simple à lire d’une part. Je trouce que le plus important est ce passage du mode « je vois une méthode » à un mode « je vois une fonction ». Une fonction traite tous les arguments et retourne un résultat. La valeur None est un résultat valide. Cela permet de gérer les cas limites, d’améliorer la qualité de votre code, et de s’approcher de la vision « fonction » de Scala.
Pattern Matching et les listes
Un petit aperçu des possibilités de Scala maintenant sur le traitement des Lists avec la notion de Pattern Matching. Si par exemple je souhaite effectuer la somme de toutes les valeurs paires d’une Liste, je peux utiliser pour cela une expression simple :
object TestPattern { def main(args: Array[String]) { val myList = List(1, 2, 3, 4, 5, 6) val emptyList = List() val emptyList = Nil println("Sum of Evens in list: " + sumEven(myList)) println("Sum of empty list: " + sumEven(emptyList)) println("Sum of a Nil : " + sumEven(nullList)) } def sumEven(in: List[Int]): Int = in match { case Nil => 0 // liste vide case x :: rest if x % 2 == 0 => x + sumEven(rest) case _ :: rest => sumEven(rest) } } // Execution Sum of Evens in list: 12 Sum of empty list: 0 Sum of a Nil : 0
Si la liste est vide (Nil) alors la fonction retourne 0. Si par contre nous avons un entier, la fonction vérifie s’il s’agit d’un nombre pair pour ensuite l’additionner. Le plus intéressant est la ligne 17. Le caractère underscore signifie « n’importe quelle valeur du début de la liste à la valeur de la variable rest« . C’est en quelques sortes une plage de valeur dynamique. Ici nous prenons la première valeur de la liste et nous appelons de manière récursive la fonction sumEven.
A noter qu’ici x et rest ne sont pas des mots clés spéciaux, simplement le nom de variables. Vous pourriez les appeler courant et restant, cela fonctionnerait aussi.
// suite def sumEven(in: List[Int]): Int = in match { case Nil => 0 // liste vide case courant :: restant if courant % 2 == 0 => courant + sumEven(restant) case _ :: restant => sumEven(restant) }
Conclusion
C’est tout pour aujourd’hui. Je parlerai un peu de l’histoire de Scala et de son fondateur la fois prochaine. Nous verrons les Actors, une notion particulièrement intéressante en Scala. Et puis je vous parlerai aussi de 3 livres sur Scala, vous verrez par lesquels commencer pour bien apprendre et ne pas se décourager.
A bientôt !
« En Java il n’est pas possible d’utiliser des String dans des boucles switch/case. »
Il n’est pas ENCORE possible 😉
La proposition des String switch/case de Joe Darcy dans le cadre du projet Coin a été retenue pour le JDK 7.
> Cela permet de reprendre le code ci-dessus, et d’écrire : […]
Les deux versions sont fondamentalement différentes. Ton exemple Scala compile mais le type de retour n’est ni String ni Option. Le sur-type commun est Object, pas vraiment utile là où tu voulais soit String soit Option[String].
Le problème vient du fait que tu utilises le type Option et en particulier la valeur None (et non le type None au passage) comme un remplacement du pointeur null de Java. La valeur null est utilisée en Java pour représenter l’absence de valeur, ce qui est particulièrement problématique car cela ne transparait pas au typage, d’où l’erreur la plus connue en ingénierie informatique : le déréférencement nul…
On peut très bien définir et utiliser un type Option en Java mais les principales raisons pour lesquelles les gens ne le font pas (opinion personnelle) sont : 1. la syntaxe pour faire ça en Java est super lourde 2. il y a un triste problème de formation des ingénieurs sur l’intérêt du typage statique ainsi que sa compréhension.
> En Scala, tout est une instance d’une classe (sauf les méthodes).
Ouch, là c’est problématique et le concepteur de Scala risque de te tomber dessus. Le concept même de Scala est de dire que tout est objet et fonction « en même temps » :
* toute fonction est un objet : regarde du côté des traits Function0[+R], Function1[+R], etc.
* tout objet est fonction : c’est l’astuce des méthodes apply()
> Scala a aussi une représentation de void appelée Unit
Là aussi, il s’agit une méconnaissance de la programmation fonctionnelle, où toute expression a une valeur. void signifie bien « absence de valeur » là où Unit est un vrai type, habité par seulement un individu : (). C’est un moyen pratique de spécifier qu’une expression a des effets de bords.
> Unit se note simplement avec une parenthèse ouvrante/fermante : ()
Unit est un type alors que () est une valeur.
> En Java, nous sommes habitués à utiliser null lorsque nous souhaitons affecter ou vérifier la valeur d’une référence. En Scala, tout est objet comme dit plus haut. C’est donc logiquement que Scala vous encourage à utiliser le type Option pour les variables et les retours de fonctions.
Ce n’est pas le fait que Scala soit orienté objet qui compte ici, mais que Scala soit fortement typé. null ne transparait pas dans le type, ce qui ne permet pas le consommateur d’une API de s’assurer qu’une valeur peut être absente ou présente qu’il devra ou non vérifier cela… Le type Option résout ce problème. Remarque : n’importe quel étudiant qui a étudié la programmation fonctionnelle un tout petit peu sait déjà tout ça 🙂
> Notez à la ligne 4 l’utilisation de l’opérateur get qui retourne la valeur d’une option
… qui est clairement un anti-pattern (le fait que Scala permette ça est à mon avis une erreur). Et d’ailleurs ici une erreur car getURLForJobType peut retourner une absence de valeur (c’est écrit dans le type de retour, pas besoin de regarde le code).
> var nullList = NilJe pense que le choix du nom de la variable (pourquoi pas une valeur d’ailleurs ?) — et confirmé par le code autour — introduit une grosse confusion, voire une erreur. Nil n’est pas une liste null ou je ne sais quoi, c’est la liste vide. Expliquer ça nécessiterait plus qu’un commentaire mais en attendant, tu peux aller lire la documentation et vérifier dans le REPL :
[[
scala> Nil == List()
res0: Boolean = true
]]
> C’est en quelques sortes une plage de valeur dynamique.
Je ne comprends pas cette remarque.
Les deux versions sont fondamentalement différentes. Ton exemple Scala compile mais le type de retour n’est ni String ni Option. Le sur-type commun est Object, pas vraiment utile là où tu voulais soit String soit Option[String].
C’est pourquoi une bonne pratique est de spécifier les types de retour des fonctions. On documente le code pour les futurs lecteurs et on bénéficie de la vérification à la compilation.
On se serait aperçue que ni
def getURLForJobType(typeContrat: String) : String =...
ni
def getURLForJobType(typeContrat: String) : Option[String] =...
ne compilent
@Alexandre et @Jean-Michel : l’erreur est corrigée concernant la méthode getURLForJobType du premier exemple. En ajoutant le type de retour, l’erreur saute aux yeux.
J’ai aussi corrigé la définition de la liste vide (cf remarque d’Alexandre)
Pour le reste, et bien je laisse mon contenu, qui montre ce que cela donne lorsqu’en effet, quelqu’un sans passif « méga fonctionnel » découvre Scala. Et je pense que je représente une grosse majorité des personnes qui passeront à Scala dans les années qui viennent.
Merci pour vos retours
🙂
> Pour le reste, et bien je laisse mon contenu, qui montre ce que cela donne lorsqu’en effet, quelqu’un sans passif « méga fonctionnel » découvre Scala. Et je pense que je représente une grosse majorité des personnes qui passeront à Scala dans les années qui viennent.
Le problème c’est que tu ne l’annonces pas comme tel. Sérieusement, beaucoup de gens te lisent et vont retenir ce que tu dis et ça me fout les boules.
Imagine-toi lisant les propos de gens qui parlent d’agilité (au sens général) et qui faisant mal. Toi de ton côté, tu te bats difficilement pour expliquer pourquoi c’est bien, tu mouilles ta chemise en produisant plein d’articles avec des références et un jour, tu te retrouves face un gars avec de la visibilité qui raconte des choses fausses, sous couvert qu’il découvre l’agilité. Je pense franchement que tu seras énervé. J’espère que ce parallèle avec l’agilité fera mouche.
En commentant tes articles, je ne cherche pas à défendre Scala spécifiquement. Je me bats contre les propos faux ou approximatifs. Il y a un défaut flagrant de formation et d’auto-formation sur l’ingénierie logicielle en général. Je trouve fou que les gens ne sâchent toujours pas faire la différence entre un langage et un framework, définir les différents de typage, n’ont pas de notion de compilation, de design, de sémantique des langages de programmation, etc. La précision et la justesse des propos sont vraiment importants et tu ne devrais pas les négliger.
Si au moins tu mettais en haut de tes articles un disclaimer du genre « je parle de Scala mais en réalité je découvre en même temps que vous », je pense que ça serait plus simple et plus juste…
@Alexandre La première phrase de mon premier article sur Scala est transparente et claire : Je profite d’un peu de temps entre 2 missions pour me former à Scala depuis quelques temps. Je pense avoir dit clairement que je découvre.
Le souci d’avoir un Blog avec quelques lecteurs, c’est en effet comme tu le dis, le risque que des bourdes fassent du mal ou qu’un article ne traite pas correctement un sujet. Après je n’essaye pas de prétendre que je connais Scala. Si le ton de l’article n’est pas transparent, je le serai plus la fois prochaine. Je corrige dans la mesure du possible selon les commentaires, quoique ceci soit parfois difficile.
Regarde le haut de la page, j’ai ajouté un méga-disclaimer.
touilleur.karma++
Oui, c’est plein d’effets de bord mais bon parfois, faut savoir se lâcher 🙂
J’arrive un peu en retard pour les commentaires mais je pense comme Nicolas qu’il n’est pas trivial pour de gens « pensant » (c’est bcp dire et prétentieux à la fois) Java de se mettre à un langage fonctionnel.
Il y a quelques années j’adorais les langages ML style oCaml et je retrouve avec plaisir ces notions dans Scala . Néanmoins c’est dur de s’y remettre quand durant des années tu as oublié de penser même à ces fameuses notions au point de les oublier.
Alexandre, je te rejoins sur le fait de rappeler que beaucoup de personnes pondent du code sans vraiment se soucier de savoir ce qu’il y a derrière(un compilateur, hein ???). Combien sont venus à l’info durant les années fastes sans le background technique minimal. Ces derniers sont à mes yeux de gros dangers car avec Java tout est facile : tu codes comme tu veux (objet ou pas) sans te soucier de la gestion de la mémoire. Et un jour… crack badaboum.
Je vois que peu de gens autour de moi connaissent même l’existence de java.util.concurent et beaucoup préfèrent encore threader à la main. Pourquoi pas si l’on aime se faire mal ? Ainsi je me dis que tout le monde ne se mettra pas à faire du thread avec les Actors demain matin. Au risque de me répéter ces notions me font bien plus rêver que tout ce que je connais en java.
Actuellement je me bats avec Lift et quand je dis je me bats c’est pas avec le dos de la cuillère. Mon niveau en scala doit être ridicule, plein d’erreurs et c’est uniquement grâce à votre aide et de celle de tous les ScalaFans que des développeurs java pourront comprendre les subtilités de Scala (qui n’est pas qu’une nouvelle syntaxe de Java comme je l’ai souvent entendu…)
Ce blog pour moi est une super bonne initiative. Qu’il y ait des erreurs c’est pas grave car je les aurais aussi commise. Vos réponses nous ouvrent les yeux.
@Alexandre
Wrong! Nicolas was right, methods are NOT functions. And no, it is not enough to implement apply to turn an object into function 🙂
http://gist.github.com/372254
I guess most remarks (except maybe the unit type) are not important since Nicolas declared that he is learning the language, and this is very useful for people that are going through the same journey.
What is problematic and dangerous however, is when someone claims to be precise and prefectly correct and then only adds noise with mistakes to the blog post in an arrogant tone.
Just my 2 cents.