Les Closures…
Ce terme claque comme un vieux chewing-gum américain dans chacune de nos conversations de Geeks. C’était le mot à placer dans une conversation à Devoxx 2009 avec le JDK7. Alors voyons un peu de quoi il s’agit. J’en profite aussi pour présenter la version Groovy, avec un peu de code. Je me suis inspiré du livre Groovy in Action pour vous expliquer les Closures en Groovy, ce qui permet de comprendre leurs utilités en Java.
Prenons un exemple simple en Java, pour tout d’abord expliquer quelques principes. Vous avez certainement déjà utilisé la méthode sort de la classe java.util.Collections non ? Cette méthode statique est capable d’effectuer un tri sur une liste en utilisant un comparateur. Imaginons un instant que je sois Georges. Il souhaite ranger ses capsules Nespresso par ordre alphabétique. Voyez ce que cela donne, avec un tri insensible à la casse :
public class ClosureTouilleurExpress { public static void main(String args[]) { System.out.println("What else ?"); ListcoffeeCaps = Arrays.asList( "Ristretto", "Arpeggio", "Roma", "Cosi", "Volluto", "Livanto", "Indriya"); Collections.sort(coffeeCaps, new Comparator () { public int compare(String s1, String s2) { return s1.compareToIgnoreCase(s2); } }); System.out.println("Mes capsules Nespresso par ordre alphabetique: " + coffeeCaps); } }
L’exécution et le résultat:
What else ? Mes capsules Nespresso par ordre alphabetique: [Arpeggio, Cosi, Indriya, Livanto, Ristretto, Roma, Volluto]
En Java il n’est pas possible de passer une référence vers une méthode, mais il est possible de définir une class anonyme, comme je viens de le faire ici. Il y a cependant une limitation: je ne peux pas passer plusieurs méthodes à ma méthode sort. Il est possible de s’en sortir en définissant une interface, implémentée par un object distinct, et ensuite passer cette interface à la méthode sort… C’est un peu compliqué non ?
Je vais vous raconter la fin du film. Une Closure en Java une fois compilée, sera en fait une classe avec une seule méthode « invoke » static. Cette classe sera générée par le compilateur Java. Ne soyez pas trop triste, c’est la vie. Java ne touche pas au bytecode de la JVM. C’est un artifice d’écriture. Groovy de même. Donc tout ce que vous voyez vise à simplifier notre travail d’écriture, mais ne fera pas aller plus vite votre application. Pensez-y lors de vos dîners en ville. Closure en Java = raccourci pour exprimer un besoin, celui de travail en fonctionnel.
Groovy
Groovy propose déjà les Closures, et je trouve cela plutôt simple. Reprenons notre exemple et transformons-le en Groovy:
List coffeeCaps =["Ristretto", "Arpeggio", "Roma", "Cosi", "Volluto", "Livanto", "Indriya"] coffeeCaps.sort { String param1, String param2 -> param1.compareToIgnoreCase(param2) } println "Mes capsules Nespress par ordre alphabetique: " + coffeeCaps
Plus simple que la version en Java non ?
Si vous voulez tester ce script, vous pouvez même faire un copier-coller et lancer la console Groovy, hébergée sur Google App Engine et développée par Guillaume Laforge.
La méthode sort est implicitement disponible sur les Lists en Groovy. Lorsqu’une méthode est appelée sans arguments, il n’est pas nécessaire de préciser les parenthèses (sort au lieu de sort()). La construction d’une liste en Groovy aussi est simple, on faisait cela en Perl il y a 10 ans déjà. Enfin le plus intéressant c’est la fonction de tri. La voilà notre fameuse Closure !
La syntaxe est la suivante:
{ <arguments> -> <body de la closure> }
Les arguments peuvent être typés (String p1, String p2) ou non (p1, p2). Le corps d’une closure est du code classique, similaire à une méthode anonyme en fait. Lorsqu’une Closure n’a pas d’argument, un object it est créé implicitement. Imaginons que vous souhaitez afficher le contenu de notre liste. Il suffit pour cela d’appeler la méthode each() et de passer une closure en argument.
Ajoutons une ligne de Groovy dans la console:
List coffeeCaps =["Ristretto", "Arpeggio", "Roma", "Cosi", "Volluto", "Livanto", "Indriya"] coffeeCaps.sort { param1, param2 -> param1.compareToIgnoreCase(param2) } coffeeCaps.each{ println it } // Ici la closure n'a pas d'argument, le signe -> n'est pas indispensable
Et exécutons ensuite le tout:
Arpeggio Cosi Indriya Livanto Ristretto Roma Volluto
La méthode each() de la Liste appelle la méthode de la closure pour chacun des éléments de la liste. L’itération de la liste donne un curseur (it) que j’utilise pour afficher directement la liste. Simple non ?
Une Closure en Groovy finalement c’est un objet, que l’on peut passer en argument d’une méthode, affecter à une instance, bref aussi facile à manipuler qu’un Object classique. Dans l’exemple ci-dessous, j’ai créé un objet Closure avec l’algo de tri par ordre alphabétique. Regardez comment la méthode sort() prend en argument cette Closure. Je vous laisse imaginer les possibilités que cela vous offre.
Closure rangeCapsule = { String p1, String p2 -> p1.compareToIgnoreCase(p2)} List coffeeCaps =["Ristretto", "Arpeggio", "Roma", "Cosi", "Volluto", "Livanto", "Indriya"] coffeeCaps.sort(rangeCapsule) //je passe la closure en argument de la methode sort() coffeeCaps.each{ println it }
Est-ce que Groovy a réellement des Closures ? D’un point de vue technique, les Closures deviennent des Classes, qui seront ensuite compilées. Une Closure n’est jamais une méthode en Groovy. Mais le principe est vraiment pratique. Pour vous aider à comprendre par rapport à Java, imaginez en fait une class avec une méthode static, comme cet exemple du site Groovy:
public class MyMath { public static int square(int numberToSquare){ return numberToSquare * numberToSquare; } } ... ... int x, y; x = 2; y = MyMath.square(x); // y will equal 4.
La même chose en Groovy devient:
// Exemple complet Closure test={ numberToSquare -> numberToSquare * numberToSquare } println test(2) // affiche 4
Et Java ?
Mark Reinhold’s a publié un billet sur son blog, et il semble qu’il n’y ait plus que 3 propositions pour ajouter le concept de Closure au langage Java dans le JDK7 prévue pour 2010 ou 2011. Les 3 propositions retenues sont CICE, FCM et BGGA. Une série d’articles très bien écrits sur Developez.com vous donne une présentation détaillée de chacune des propositions.
CICE (spec), par Bob Lee, Doug Lea et Josh Bloch.
FCM (spec), par Stephen Colebourne et Stefan Schulz.
BGGA (spec), par Gilad Bracha, Neal Gafter, James Gosling et Peter von der Ahé.
BGGA
La proposition BGGA est la plus complète. Elle est parfois un peu complexe à comprendre, mais le principe est de déclarer d’une part les paramètres d’entrées/sorties, et ensuite de spécifier l’algo.
L’exemple de la fonction « sum » pour réaliser une addition se décomposerait de cette façon:
{ int, int => int } additionne = { int x, int y => x+y };
L’usage de la closure type BGGA serait alors:
int resultat=additionne.invoke(2,5); // resultat vaut 7
La closure est définie en 3 zones.
1) Une zone où l’on spécifie le type de paramètres en entrées, le type retourné
{ int,int => int}
2) le nom de la closure
additionne
3) la définition de la closure
{ int x, int y => x+y }
La version FCM est différente, voici ce que cela donne:
#(int(int,int)) plus = #(int x, int y) { return x + y; }
Allez lire l’excellent article d’adiGuba sur Developpez.com
FCM
La plus grosse différence est l’utilisation du caractère # (dash) qui n’est pas utilisé en Java. Quelque part, cela pourrait nous aider à repérer une Closure rapidement, pourquoi pas ?
CICE
L’implémentation se base sur la simplicité. Assez différente des 2 autres, la proposition permet d’alléger du code classique. Voyez l’exemple de tri des éléments d’une liste par taille :
Listls = ... ; Collections.sort(ls, new Comparator () { public int compare(String s1, String s2) { return s1.length() - s2.length(); } });
Avec la proposition CICE:
Listls = ... ; Collections.sort(ls, Comparator (String s1, String s2){ return s1.length() - s2.length(); });
Revenons à Groovy
Et juste pour le fun, voici l’implémentation en Groovy:
List coffeeCaps =["Ristretto", "Arpeggio", "Roma", "Cosi", "Volluto", "Livanto", "Indriya"] coffeeCaps.sort { String p1, String p2 -> p1.length() <=> p2.length() } println "Par ordre croissant: " + coffeeCaps
Ce qui donne:
Par ordre croissant: [Roma, Cosi, Volluto, Livanto, Indriya, Arpeggio, Ristretto]
Conclusion
Je vous ai présenté le principe des Closures en me basant sur Groovy, dont la syntaxe est simple. J’avoue que j’ai un peu peur de ce que nous sommes entrain d’essayer de faire au langage Java en lui-même.
Les Closures n’étaient pas nécessaires jusqu’à maintenant en Java. Simplement, les vieux développeurs s’ennuient. Et comme ils sont nombreux et qu’ils tiennent à ce que l’on se souvienne d’eux, ils tentent de proposer des innovations et des idées. Certaines sont bonnes, d’autres sont discutables. Je pense que les Closures sont une bonne idée, mais je suis plutôt circonspect quant à sa bonne utilisation par un junior devant son écran… J’ai peur. Réflexe de vieux.
Lorsque le problème survient, avec son domaine d’application, je pense qu’il est plus intéressant d’apprendre un autre langage qui tourne sur la JVM comme Scala ou Groovy, plutôt que d’attendre un hypothétique consensus sur les Closures. Alors oui, il y aura des Closures en Java, la syntaxe sera simple ou non, mais je préfère maintenant investir mon temps à apprendre Scala et Groovy.
Je trouve dommage qu’un fois de plus, on ne résume les closures qu’à du sucre syntaxique pour définir et/ou passer une fonction en paramètre.
Une closure est une fonction qui admet des variables libres, c’est-à-dire non déclarée dans celle-ci. La résolution de ces variables dépend du contexte (on parle de capture de contexte) et c’est *là* que ça devient vraiment intéressant. Ça a le goût et la texture d’un langage dynamique m ais il n’en ai rien : on peut parfaitement compiler la fonction une bonne fois pour toute, il faut savoir où on trouvera la variable capturée lors de l’exécution… En Java, on peut émuler partiellement ce comportement mais au prix d’une complexité qui décourage de s’y mettre.
Je n’ai personnellement pas pris le temps de suivre les rumeurs pour Java 7 mais à regarder cette synthèse [1] et le « Peut-être pas » en face de « Accès aux variables locales non-final » ainsi que les différents liens sur les différentes propositions permettent de dire que les « vraies closures » dans Java, c’est pas pour demain…
En Scala, on ne s’en rend pas compte mais il y a des closures partout. Je viens de regarder dans Groovy : c’est bon, y a pas arnaque sur la marchandise 🙂
Alexandre.
[1] http://blog.developpez.com/adiguba/p8396/java/7-dolphin/info-closures-java-7/
@Alexandre : j’avais prévenu dès le titre que je n’y connaissais rien. J’ai fait le choix de passer par le code, pour parler de manière concrète à un développeur qui est entrain de me lire en trempant son exemplaire du Touilleur Express dans son café du matin. Pour la partie théorique, les articles sur Wikipedia par exemple sont bien faits. J’ai repensé à ta présentation au Paris JUG. J’imagine comment les Closures sont implémentées en Groovy et en Scala, mais je serai intéressé par un article sur « comment cela fonctionne » derrière, au niveau de la JVM. J’imagine une solution assez simple. En tous les cas merci pour le complément. Les commentaires sont aussi importants que l’article et permettent à tous de participer. A bientôt au Paris JUG.
@Nicolas: et si tu installais le plugin WP « Syntax Highlighter and Code Prettifier Plugin for WordPress », ça rendrait le code de tes exemples ‘achement plus lisible ([mode fan= »on »]et ton blog serait encore plus mieux :)[/mode])
@Christophe c’est fait. en fait j’utilisais le plugin Google syntax highlighter mais il ne marche pas avec ce nouveau style. J’ai pris celui que tu m’as conseillé. Merci !
Perso, ce que j\’aimerais, c\’est un auto-boxing des méthodes en objets implémentant une interface, comme je l\’indique ici : http://www.jroller.com/dmdevito/entry/next_jdk_wishlist_3_a
Cela couterait pas cher et cela permettrait d\’améliorer un peu les choses.
En gros, si j\’ai une méthode :
Object myProcessing(Object e) { ... }
Méthode dont la signature est compatible avec l\’interface :
public interface UnaryOperator { Object eval(Object arg); }
avec, par ex, l\’utilisation suivante de cette interface :
public class AbstractList ... { public void map(UnaryOperator op) { ... } }
Alors j\’aimerais pouvoir simplement écrire :
mylist.map(myProcessing);
Avec auto-boxing automatique de \ »myProcessing\ » en un objet implémentant l\’interface UnaryOperator.
Ce serait un plus, et une avancée qui pourrait obtenir un consensus plus facilement que les différentes propositions des closures.