septembre
2008
Dans ce troisième volet de la série « Introduction à Scala », je vais présenter la notion de méthodes et fonctions de Scala, qui est bien plus puissante que celle dans Java, dans la mesure où les fonction dans Scala sont des objets à part entière, pouvant être stockées dans une variable, passées ou retournées par une autre méthode, etc.
1. Présentation
La définition d’une méthode en Scala se présente comme suit:
[private|protected] [override] def nom [paramètres] [type de retour] [corps]
Ici, ce qui est entre [ ] dénote un élément optionnel.
Notez l’utilisation du mot clé def pour définir une fonction.
Dans Scala, et tout comme pour les définitions des classes, la visibilité par défaut est public, à moins qu’on précise explicitement la visibilité privée (via private) ou protégée (via protected).
Tout comme Java, les fonctions dans Scala sont virtuelles par défaut. Par contre, et à l’inverse de Java, pour redéfinir une méthode virtuelle, il faut le préciser explicitement avec le mot clé override, ce qui permet d’éviter quelques séances intensives de débogage pour comprendre l’origine de bugs bizarroïdes (C’est la même approche que celle prise par C#).
La partie paramètres d’une fonction est un peu différente de celle de Java et ce pour diverses raisons:
– Tout d’abord, à cause de la convention Pascal pour les déclarations (nom:Type)
– Mais aussi le fait que les parenthèses sont complètement optionnelles pour les méthodes sans arguments
– Et enfin, une fonction peut déclarer plusieurs listes de paramètres (j’y reviendrais plus tard)
J’ai déjà montré quelques exemples de méthodes avec les getters et les setters.
En voici une autre:
Cette méthode augmente l’age de la personne de n années.
Son invocation n’a rien d’exceptionnel, et se présente comme suit:
Cette méthode montre entre autres comment le type de retour est optionnel. D’ailleurs, elle ne le déclare pas. La définition exacte aurait été:
2. De l’objet au fonctionnel
Dans cette partie, je vais montrer l’aspect fonctionnel de Scala à travers un exemple où on part d’une implémentation à base des paradigmes de l’OO, puis en convertissant vers une approche plus fonctionnelle.
2.1. La version OO
Il s’agit tout simplement d’implémenter une méthode qui effectue un traitement d’une façon répétitive mais périodique.
Dans l’approche OO (et Java plus précisément), on commencerait par créer une interface qui définit une méthode.
Puis, on créerait une méthode qui prend cette interface comme paramètre ainsi que la période de répétition.
Voici une implémentation de cette approche en Scala :
J’ai défini un trait Action (la chose qui ressemble le plus à une interface dans Scala) avec une méthode doSomething.
J’ai ensuite crée une implémentation PingAction de ce trait qui ne fait qu’afficher le message « ping » dans la sortie standard (j’ai utilisé object au lieu de class tout simplement pour avoir un singelton).
J’ai aussi défini une méthode doPeriodically qui prend un Action et un entier comme paramètres, et qui répète l’exécution de l’action indéfiniment tout en « dormant » à chaque itération.
Voici à titre de comparaison la version Java de ceci:
2.2. La version fonctionnelle
En relisant le code précédent, on constate que malgré le fait qu’on a seulement besoin d’une méthode à exécuter périodiquement, on est obligé de construire plein de plomberie sans réel apport pour pouvoir y arriver, i.e. définir une interface, faire implémenter cette interface par une classe, et passage d’une instance de l’implémentation à la méthode d’exécution.
Or, ceci n’est aucunement nécessaire dans un langage fonctionnel comme Scala, où une méthode est un type de données comme les autres, c’est à dire que l’on peut écrire une méthode qui prend une méthode comme paramètre.
La définition du type d’une méthode en Scala se présente comme suit :
Par exemple, une fonction mathématique comme le sinus ou le consinus admet le type suivant:
(Float) => Float
C’est à dire que c’est une fonction qui prend un float (réel) en paramètre et retourne aussi un réel.
Pour revenir à notre exemple, la méthode doPeriodically a besoin d’une méthode qui ne prend aucun paramètre et qui ne retourne rien, ce qui correspond au type :
() => Unit
Unit en Scala correspond au void de Java.
Voici donc une nouvelle version du programme précédent utilisant ces nouvelles données :
En gros, plus d’interface à implémenter ou d’objets à passer : on passe directement une méthode telle quelle à une autre fonction (via son nom), et celle-ci peut dès lors l’invoquer (toujours via son nom).
3. Concrètement
En fait, la notation « (params) => type-du-résultat
» n’est que du sucre syntaxique fourni par Scala pour simplifier l’utilisation des fonctions.
Concrètement, les fonctions sont représentées par un ensemble de traits FunctionN où N dénote le nombre de paramètres qu’elle prend.
Par exemple, le type de fonction ne prenant pas de paramètres est représenté par Function0[+R], où le R dénote le type de retour, tandis qu’une fonction prenant un seul paramètre est représentée par Function1[-T1, +R], où T1 est le type du paramètre et R le type de retour.
Dans tous les cas, ces traits définissent une méthode abstraite apply qui dans le cas de Function1 par exemple est définie comme suit:
apply (v1 : T1) : R
4. Higher order functions
En utilisant ce qu’on vient de voir, il devient possible de coder des trucs vraiment intéressant avec les fonctions, genre une méthode qui combine deux fonctions en une nouvelle fonction par exemple.
Ce type de fonctions s’appelle fonctions d’ordre supérieur, ou « Higer Order Functions« .
A titre d’exemple, voici une implémentation en Scala de la fonction « rond », dénoté » par « o » en Mathématiques.
Petit rappel : f o g (x) = f(g(x))
Je commence par déclarer un nouveau type MathFunc qui est (Float) => Float
, où encore une fonction prenant un float comme paramètre et retournant un float (C’est strictement optionnel, et uniquement dans le but de simplifier le code).
Je code ensuite la fonction rond qui prend deux MathFunc f et g en paramètre et retourne un MathFunc, i.e une nouvelle fonction de type MathFunc et qui devrait correspondre ) f o g.
Comme résultat, cette méthode retourne une nouvelle instance de Function1[Float, Float] dont la méthode apply retourne f(g(x)).
Dans la méthode main, je montre un exemple d’utilisation de cette fonction en combinant deux fonctions f et g que j’ai défini.
Remarque : Veuillez notez que la composition de fonctions que je viens de montrer est déjà implémentée dans Scala par les traits Function*, via la méthode compose.
Maintenant, à titre d’exemple (que je ne vais pas expliquer), et juste pour montrer la puissance de Scala et sa flexibilité, voici un bout de code qui permet de remplacer le « rond(f, g) » de l’exemple précédent par la notation plus naturelle « f o g » (La ligne surlignée en bleu clair):
5. Currying
Cette section a été corrigée suite à une remarque de SpaceGuid. Merci à lui
Le currying est une technique qui consiste à fixer la valeur de quelques uns des paramètres d’une fonction pour en créer une nouvelle qui prend les autres paramètres et qui retourne le résultat de l’originale.
Plus abstraitement, étant donné une fonction de type (X x Y) => Z (prend un paramètre de type X et un autre de type Y, et retourne un résultat de type Z), sa currification la transforme en fonction de type X => (Y => Z), c’est à dire une fonction prenant un paramètre de type X, et retournant une fonction qui prend un paramètre de type Y et retournant un résultat de type Z.
Par exemple, étant donné une fonction mul qui prend 2 paramètres x et y et qui retourne x*y (très intéressante et puissante comme fonction :D), on peut en dériver une fonction double qui prend un seul paramètre x et qui retourne son double (2*x) en fixant la valeur de y à « 2 ».
Le currying est très simple et intuitif à mettre en place avec Scala. Jugez vous en par vous même à travers cet exemple :
En gros, j’ai fait le currying de la fonction mul(x)(y) en fixant la valeur du paramètre x à 2 (via mul(2)), ce qui donne naissance à une nouvelle fonction qui ne prend plus qu’un seul paramètre et qui le multiplie par 2.
6. Exemple: Collections Scala
Les collections de Scala utilisent intensivement l’aspect fonctionnel du langage, ce qui permet de réaliser des traitements complexes sur une liste d’une façon intuitive et fluide.
Je vais commencer par montrer quelques exemple simples, pour finir avec d’autres plus complexes.
6.1. Exemples simples
Les commentaires expliquent ce que fait chaque méthode.
Notez que j’ai usé du type-inference du compilateur Scala pour pouvoir écrire
(x, y) => x + y
par exemple en omettant les types des arguments :
6.2. Ne joues pas au plus « Perl » avec Scala !
Maintenant, je vais présenter les même exemples de la section précédente, mais en introduisant la « Perl touch », i.e. quelques hiéroglyphes magiques qui réduisent considérablement la taille de code (mais au risque de réduire autant la lisibilité du code).
L’hiéroglyphe magique en question dans Scala est le « _ », qui représente le joker (% dans le SQL, ? dans les expressions régulières, * dans les noms de fichiers, etc.).
Il a plusieurs connotations dans Scala et ce selon où il est utilisé.
Dans le cas des closures, il permet de représenter un paramètre d’une fonction.
Par exemple, « (x) => 2 * x
» peut être ré-écrit en « 2 * _
« , et le compilateur Scala est assez malin pour savoir ce qui va dans le slot « _ ». De même, le compilateur peut se débrouiller avec un truc comme « _ + _
» qui correspond à « (x, y)=> x + y
» peut devenir .
Ainsi, le code précédant devient :
6.3. Exemple plus complexe
No comment ! en fait si, un commentaire : dans Scala, ça bloque pas, c’est très fluide
Conclusion
Voilà, ainsi s’entame le troisième volet de l’introduction à Scala qui a servi, je l’espère, à présenter l’aspect fonctionnel de Scala via le.
Dans le prochain volet, ce serait le tour des traits, une sorte d’interfaces Java aux stéroïdes.
Astuce: pour tes types dans le texte tu peux utiliser la flèche → ou bien la flèche ⇒ .
Avec ses extensions, Java ne fait que repousser le moment où il sera obsolète tout en avançant le moment où il devient une montagne de rustines frustrantes et obfuscatoires.
Pour moi la question c’est surtout est-ce que Scala va assez loin pour que la JVM ne soit pas ringuardisée par F#, et pour l’instant que trouve que oui Scala tient très bien la route.
Il faut dire que Microsoft s’y entend pour ajouter moultes extensions à son F#, parfois élégantes, parfois farfelues, le plus souvent dot-netsques, et ça contribue beaucoup à donner à Scala une image de quelque chose de bien plus cohérent, qui ne s’éparpille pas dans tous les sens.
Je pense que Scala est actuellement le meilleur langage fonctionnel pour un débutant qui vient de Java ou du C.
@natha:
>Est-ce que l’évolution du Java pour ces prochaines années sera la disparition du langage au profit d’un langage tel
>Scala mais la conservation de toutes les librairies existantes ? On garderait la force de Java tout en mettant en
>place un langage qui renouvelle l’OO !
Amen ! mais au lieu de ça, on voit que le langage Java se fait toute sorte de greffes et d’extensions …
Mais bon signe : Sun s’investit de plus en plus dans ces langages (Groovy, JRuby, JPython) … mais pas encore dans Scala
J’adore !
J’ai vu la conf de présentation de Scala à Jazoon 2008, c’est vraiment impressionnant et ça donne envie de s’y mettre.
Est-ce que l’évolution du Java pour ces prochaines années sera la disparition du langage au profit d’un langage tel Scala mais la conservation de toutes les librairies existantes ? On garderait la force de Java tout en mettant en place un langage qui renouvelle l’OO !
De quoi rester enthousiaste pour l’avenir
Merci pour ces billets en tout cas.