décembre
2009
Après BGGA et CICE, voici une présentation de la proposition de closures FCM qui se place exactement entre les deux précédentes : à la fois plus simple que BGGA mais également plus complète que CICE.
FCM se présente comme un compromis entre les deux, en proposant une approche plus simple sans pour autant trop perdre de possibilités.
De plus, tout comme « BGGA« , la proposition « FCM » propose également un prototype sur lequel je me suis basé dans cet article (FCM-2008-02-25.zip), mais malheureusement il n’implémente pas toutes les fonctionnalités de la proposition…
Avant de commencer, je tiens à rappeler qu’il ne s’agit pour le moment que d’une proposition, ce qui fait que ce qui est décrit dans ce billet pourrait évoluer d’ici que les closures soit ajoutés au langage (si c’est le cas un jour). Ce billet a surtout pour objectif de présenter les possibilités que cela pourrait apporter au langage. De même, si ce n’est pas déjà fait, la lecture préalable du billet concernant la proposition BGGA est fortement recommandé car il présente les notions de manière plus approfondi !
Bonne lecture !
Sommaire / Accès rapide :
- Closures ?
- Method Types
- Accès aux variables locales
- Closures Conversions
- Method references
- Member literals
- Unrestricted Closures
- Structures de contrôles
- Structures de contrôle étendus (avec paramètres de closures)
- Structures de contrôle de boucle
- Conclusion
Closures ?
Pour être précis, la proposition FCM utilise le terme « anonymous inner method« , qui correspond à une écriture simplifié des classes anonymes.
Par exemple, pour reprendre l’exemple d’une closure qui effectuerait la somme de deux valeurs entières, on obtiendrait le code suivant :
#(int x, int y) { return x + y; }
Le #
est spécifique à la proposition CICE, On retrouve ensuite la liste des paramètre de la méthode entre parenthèses, suivi d’un bloc entre accolades permettant de délimiter le code de la manière la plus standard qui soit. On remarquera également, et contrairement à la proposition BGGA, la présence du mot-clé return qui s’applique bien ici au code de la méthode de la closure, et non pas à la méthode appelante.
Le choix du caractère #
s’est fait pour deux principales raisons :
- Il n’est pas utilisé dans le langage, ce qui permet d’éviter les confusions avec d’autres syntaxes existantes et de limiter les conflits du parser.
- Ce n’est pas vraiment une nouvelle syntaxe car il est déjà utilisé de manière similaire dans la documentation javadoc.
Method Types
Les « Method Types » ne sont ni plus ni moins que l’équivalent des « Function Types » de BGGA, et permettent de déclarer dynamiquement un type représentant une seule et unique méthode :
#(int(int, int)) plus;
Ici, on déclare une référence nommée « plus« , qui représente une « function type » prenant deux paramètres de type int, et retournant un int.
Les types sont déclarés dans le même ordre que dans la signature d’une méthode : le premier int représente la valeur de retour, et ceux qui suivent entre parenthèses correspondent aux paramètres. Le tout englobé dans une paire de parenthèse, dont l’objectif est principalement d’éviter les ambiguitées avec les autres syntaxes de la proposition (voir plus loin).
Le tout correspondrait donc à la définition d’une interface de la forme suivante :
public interface III {
public int invoke(int x, int y);
};
Au final la déclaration et l’initialisation d’une closure FCM pourrait ressembler à ceci :
#(int(int, int)) plus = #(int x, int y) { return x + y; };
On est alors libre de manipuler la référence « plus » comme n’importe quelle référence. On peut donc la passer à d’autre méthode et appeler sa méthode invoke()
autant de fois que neccessaire…
Les « Method Types » et les closures peuvent bien sûr prendre plusieurs forme, et remonter des exceptions, par exemple :
// Définition d'une "function type" prenant un double en paramètre,
// et retournant une String
// Implémentation d'une "closure" qui formate le double en String
#(String(double)) format = #(double d) { return String.format("%.2f", d); };
// Définition d'une "function type" prenant un int en paramètre,
// et pouvant remonter une exception
// Implémentation d'une "closure" qui fait une pause de 'n' secondes
#(void(int) throws Exception) sleep = #(int n) { Thread.sleep(n*1000); };
// Définition d'une "function type" pouvant remonter une exception
// Implémentation d'une "closure" remontant une exception dans tous les cas
#(void() throws IOException) boom = #() { throw new IOException("Boom") };
// Définition d'une "function type" sans paramètre ni type de retour
// Implémentation d'une "closure" qui affiche un "Hello World !"
#(void()) hello = #() { System.out.println("Hello World !"); };
// Définition d'une "function type" qui retourne une valeur entière
// Implémentation d'une "closure" retourne un nombre entre 0 et 100
#(int()) number = #() { return (int)(Math.random()*100.0); };
Note : lorsque la closure ne comporte aucun paramètre, il est possible de se passer des parenthèses vides dans la définition de « anonymous inner class« , ce qui donnerait dans le dernier exemple ci-dessus :
#(int()) number = #{ return (int)(Math.random()*100.0); };
La spécification FCM ne met pas vraiment en avant les « method types« , et les présente même comme optionnel (en leurs priviliégiant l’utilisation d’interface – voir plus loin la section Closures Conversions). De plus cela n’étant pas implémenté dans le prototype il n’est pas possible de savoir s’il y a un équivalent aux notions de covariance et de contravariance présentent dans BGGA.
Accès aux variables locales
FCM permet également l’accès total aux variables locales à l’intérieur des closures et inner-classes, sans utilisation de mot-clé particulier. Si cela devrait se faire dans les mêmes conditions que BGGA (sans l’annotation @shared
), on reste encore une fois dans le flou puisque la spécification est relativement succincte et que le prototype n’implémente pas cela.
Toutefois il devrait logiquement être possible d’utiliser un code comme celui-ci :
int count = 0;
#(void()) print = #{ System.out.println( count++ ); };
print.invoke(); // Affiche : 0
print.invoke(); // Affiche : 1
print.invoke(); // Affiche : 2
print.invoke(); // Affiche : 3
System.out.println("count = " + count); // Affiche : count = 4
La seule particularité de FCM vient d’une annotation @ConcurrentLocalVariableAccess
à utiliser sur les paramètres des méthodes afin d’indiquer que le code de la closure sera exécuté dans un thread séparé, et nécessitera donc un synchronisation… mais son utilisation précise reste confuse.
Closures Conversions
Tout comme BGGA, FCM met en place la conversion de closure afin de les rendre compatible avec les interfaces « mono-méthode » et les classes « mono-méthode-abstraite »
De ce fait, si on reprend notre premier exemple qui se contentait de sommer deux int :
#(int(int, int)) plus = #(int x, int y) { return x + y; };
On pourrait très bien utiliser une interface standard à la place de la « method types » :
public interface IntPlus {
int add(int x, int y);
}
Ce qui nous donnerait :
IntPlus plus = #(int x, int y) { return x + y; };
La closure est alors automatiquement convertie en une implémentation de l’interface, et le code de la closure sera alors accessible via la méthode add() défini par l’interface.
FCM met vraiment cela au premier plan, bien plus que les « method types » qui sont presque optionnel (Stephen Colebourne, l’un des auteurs de la proposition, envisage parfaitement l’implémentation de FCM sans les « method types« ).
En fait la proposition FCM prévoie surtout de remplacer les classes anonymes par des « inner method« , mais déconseille l’utilisation des « method types » au profit d’une interface ou classe clairement définit. Ceci bien sûr afin de faciliter la lecture du code.
De plus, la proposition FCM se distingue également par la notion de « named inner methods » qui permet d’utiliser la syntaxe des closures pour hériter d’une classe concrête, en précisant l’unique méthode à redéfinir. Il suffit pour cela re spécifier le nom de cette méthode juste avant la déclaration des paramètres :
// La classe WindowAdapter défini 10 méthodes concrêtes,
// mais on peut quand même utiliser les closures FCM pour redéfinir une seule et unique méthode :
WindowAdapter adapter = #windowClosed(MouseEvent e) { /* ... */ };
Ce qui serait grosso-modo l’équivalent du code suivant :
WindowAdapter adapter = new WindowAdapter() {
@Overrides
public void windowClosed(MouseEvent e) {
/* ... */
}
};
Bien sûr si on veut hériter d’une ou plusieurs méthodes, on devra toujours passé par des classes anonymes. Mais ce n’est pas le but des closures
Method references
Ceux qui ont bien suivi la présentation sur BGGA auront noté que la notion de « method references » vient de FCM. Il est donc logique qu’on la retrouve ici sous la même forme :
Nom # identifier ( TypeList )
Où :
Nom
correspond soit à un nom de classe, soit à une référence valide.identifier
correspond au nom de la méthode.TypeList
correspond à la liste des paramètres de la méthode.
Pour rappel cette syntaxe permet d’utiliser une méthode existante comme une closure.
Il est donc possible d’associer une méthode à une « method type » :
#(void(Component, Object)) showMessage = JOptionPane#showMessageDialog(Component, Object);
Ou plus globalement directement à une interface compatible :
Runnable doNothing = Thread#yield();
La proposition distingue clairement les trois types de références, tel qu’il ont été repris par BGGA :
- Static method references, qui permet donc de transformer une méthode statique :
#(double()) ref = Math#random();On conserve logiquement la même signature de part et d’autre.
- Instance method references, qui permet d’associer une méthode d’instance. La « method type » (ou l’interface) associée devra alors posséder un paramètre supplémentaire qui correspondra à l’instance sur laquelle on souhaite appeler la méthode :
#(int(List, Object)) ref = List#indexOf(Object); - Bound method references, qui permet d’associer une méthode d’instance avec une référence existante (on obtient de nouveau une signature similaire, et tous les appels se feront sur l’instance référencée) :
List<String> myList = ...;
#(int(Object)) ref = myList#indexOf(Object);A noter également l’existence de la notation #method() qui est l’équivalent de this#method(), et qui permet donc de référencer une méthode de la classe courante.
FCM et défini également la notion de Constructor references, qui permet de référencer un constructeur de la même manière. La syntaxe est identique si ce n’est que le nom de la méthode est omis :
#(Integer(int)) ref = Integer#(int);
Ce qui pourrait s’avérer fort pratique pour implémenter rapidement et simplement des « fabriques » :
ThreadFactory factory = Thread#(Runnable);
Mais la proposition FCM va beaucoup plus loin en apportant la notion de « Member literals« …
Member literals
La problématique est un peu éloigné des closures, mais assez intéressante puisqu’elle consiste à améliorer l’utilisation de la reflection. En effet, l’API de réflection de Java est très puissante et permet beaucoup de chose, mais possède un très gros problème : toutes les vérifications (ou presque) sont effectué à l’exécution. En effet tous les noms de méthodes, constructeurs ou d’attributs sont généralement passé en utilisant leurs noms via une String, ce qui fait que le compilateur n’a aucun moyen de vérifier cela. Toutes les vérifications s’effectueront donc à l’exécution, ainsi que les éventuelles erreurs…
L’objectif des « Member literals » est de proposer une alternative sécurisé aux méthodes getMethod(), getConstructor() et getField(), tout comme nous avons déjà une version sécurisé de Class.forName(« Name ») via le litéral Name.class (le compilateur peut vérifier l’existence de la classe et certaines contraintes).
Les « Member literals » utilisent exactement la même syntaxe que les « Method references« , et permettent de référencer les différents composants d’une classe (constructeurs, méthodes et attributs) d’une manière sécurisé (dans le sens où le compilateur pourra vérifier leurs existences dès la phase de compilation).
- Les Method literals permettent donc de référencer une méthode afin d’obtenir l’instance de java.lang.reflect.Method correspondant :
// Que ce soit pour une méthode static :
Method min = Math#min(int, int);
Method sort = Collections#sort(List, Comparator);
// Ou une méthode d'instance :
Method shortValue = Integer#shortValue();
Method equals = Object#equals(Object);Ce qui est l’équivalent du code suivant :
// Que ce soit pour une méthode static :
Method min = Math.class.getMethod("min", int.class, int.class);
Method sort = Collections.class.getMethod("sort", List.class, Comparator.class);
// Ou une méthode d'instance :
Method shortValue = Integer.class.getMethod("shortValue");
Method equals = Object.class.getMethod("equals", Object.class); - Les Constructor literals permettent de référencer un constructeur de manière précise, et sont donc assignable vers le type java.lang.reflect.Constructor
:
Constructor<ArrayList> newList = ArrayList#(int);
Constructor<JButton> newList = JButton#(String, Icon);Ce qui reviendrait à écrire en utilisant l’API de réflection :
Constructor<ArrayList> newList = ArrayList.class.getConstructor(int.class);
Constructor<JButton> newList = JButton.class.getConstructor(String.class, Icon.class); - Enfin, les Field literals permettent quand à eux d’obtenir une instance de java.lang.reflect.Field représentant un attribut:
// Que ce soit pour un attribut static :
Field out = System#out;
Field exitValue = JFrame#EXIT_ON_CLOSE;
// Ou bien un attribut d'instance :
Field width = Dimension#width;De même, cela se fait actuellement via le code suivant :
// Que ce soit pour un attribut static :
Field out = System.class.getField("out");
Field exitValue = JFrame.class.getField("EXIT_ON_CLOSE");
// Ou bien un attribut d'instance :
Field width = Dimension.class.getField("width");
Tout ceci permettant de sécurisé un peu plus l’utilisation de la reflection, en permettant au compilateur de vérifier la cohérence des litéraux, et donc en évitant de reporter ces erreurs lors de l’exécution. Le tout bien sûr en respectant les règles de visibilités standards de ces éléments selon le contexte…
Structures de contrôles
Contrairement à BGGA, la proposition FCM ne permet pas de définir de nouvelle structure de contrôle et se limite aux closures en elle même.
Toutefois, à l’instar de CICE et ARM, la proposition FCM est associé à la proposition JCA qui propose l’ajout d’une nouvelle syntaxe permettant de redéfinir des structures de contrôles.
Reprenons comme exemple un éventuelle bloc synchronizedWith qui fonctionnerait de la même manière que le mot-clef synchronized, mais en utilisant un Lock
de l’API de concurrence de Java 5.0, et dont la structure s’utiliserait de la manière suivante :
Lock lock = new ReentrantLock();
synchronizedWith(lock) {
System.out.println("Synchronized !");
}
Pour cela, il faut déclarer une méthode synchronizedWith()
avec une syntaxe un peut particulière du fait que ses paramètres seront séparé en deux parties :
public static void synchronizedWith(#(void()) block : Lock lock) {
lock.lock();
try {
block.invoke();
} finally {
lock.unlock();
}
}
La méthode prend deux types de paramètre :
- Un Method Types qui correspondra au bloc de code associé à notre structure de contrôle.
- La liste des paramètres qui devront être directement passé à la méthode. Ici une instance de
Lock
en l’occurrence.
Ces deux types de paramètres sont séparés par le symbole » : » afin de bien les distinguer (à la manière du for étendu de Java 5.0).
Contrairement à BGGA, la proposition JCA simplifie au maximum son utilisation, et évite tout les cas particulier :
- Les méthodes représentant une structures de contrôles ne peuvent pas renvoyer de résultat.
- Les exceptions sont automatiquement propagé vers la méthode appelante.
Structures de contrôle étendus (avec paramètres de closures)
Il est également possible d’utiliser des structures de contrôles dont le bloc de code attend des paramètres.
Pour cela nous allons reprendre l’exemple d’une structure with qui permettrait d’utiliser un objet « closeable » en le fermant proprement à la fin du traitement, et qui s’utiliserait de la manière suivante :
with(FileInputStream in : new FileInputStream("x")) {
/* code */;
}
Cela nous donnerait donc une méthode/structure de contrôle de la forme suivante :
public static <C extends Closeable> void with( #(void(C)) block : C flux ) {
// Le flux est créé juste avant l'appel et reçu en paramètre
try {
// On exécute le code de la closure associée
block.invoke(flux);
} finally {
// Puis on ferme le flux dans tous les cas
flux.close(); // throws IOException
}
}
On remarquera principalement que le choix de la syntaxe permet de conserver le même ordre des paramètres dans la définition de la méthode que dans l’utilisation de la structure de contrôle.
Le bloc de code pourra accepter le code de n’importe quel bloc, et interagir avec la méthode parente (return
, throw
exception, break
/continue
par rapport à une boucle parente…)
Structures de contrôle de boucle
Il est également possible de définir des structures de contrôle de boucle permettant de reproduire le comportement du for étendus de Java 5.0. Il suffit pour cela de préfixer le premier paramètre par le mot-clef for
:
public static <T> void each(for #(void(T)) block : Iterable<T> iterable) {
Iterator<T> iterator = iterable.iterator();
while (iterator.hasNext()) {
block.invoke(iterator.next());
}
}
L’appel se fait de manière similaire à la proposition BGGA, c’est à dire en utilisant une fois de plus le mot-clef for
:
List<String> list = ...
// La syntaxe d'appel est proche de celle du for étendu de Java 5.0 :
for each(String str : list) {
System.out.println(str);
}
Désormais, les mot-clefs break
et continue
s’appliqueront à notre structure de contrôle et non pas à une boucle parente.
Il est bien sûr possible d’utiliser des bloc de code avec plusieurs paramètres, comme par exemple avec ce code permettant de parcourir les éléments d’une Map de manière distincte :
public static <K, V> void each(for #(void(K, V)) block : Map<K, V> map) {
for (Map.Entry<K, V> entry : map.entrySet()) {
block.invoke(entry.getKey(), entry.getValue());
}
}
Et qui s’utiliserait de la manière suivante :
Map<String, Date> map = ...
for each(String key, Date value : map) {
System.out.println("key = " + key + ", value = " + value);
}
J’ai déjà parlé de l’intérêt des structures de contrôle sur le billet de la proposition BGGA, et force est de constater que cette proposition JCA/FCM en conserve les grandes lignes sous une forme bien plus simple.
On remarquera toutefois, en comparaison avec BGGA, les limitations suivantes :
- Il n’est pas possible pour une structure de contrôle de renvoyer une valeur de retour.
- Il n’est pas possible d’utiliser un retour de valeur dans la closure associé à la structure de contrôle.
- Il n’est pas possible de contrôler la gestion des exceptions : les exceptions remonter dans le corps de la closures seront remontées par la structure de contrôle, apparemment sans moyen de les traiter.
Conclusion
Au final la proposition FCM, couplée à JCA pour la gestion des structures de contrôle, se révèle relativement complète en proposant de manière plus « simple » et directe la quasi-totalité des fonctionnalités de BGGA, tout en limitant fortement sa complexité.
Je dois dire qu’entre BGGA et FCM/JCA mon coeur balance, et aurait un peu tendance à pencher vers ce dernier du fait de sa simplicité.
Malheureusement à l’heure actuelle il y a très peu d’information sur le sujet : on ne sait toujours pas si Java 7 intègrera ou non les closures, si ce sera remis à une version ultérieure, ou si on devra s’en passer définitivement…
Où va Java ? Les Closures
3 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- [ fuite ] memoire
- Recuperation du nom des parametres
- Classes, méthodes private
- jre 1.5, tomcat 6.0 et multi processeurs
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- Définition exacte de @Override
- Possibilité d'accéder au type générique en runtime
- Difference de performances Unix/Windows d'un programme?
C’est assez dingue comme on peut être loin des closures de ruby.
sig.
Oui malgré qu’elle soit un peu plus limité, je trouve que FCM s’intègre beaucoup mieux dans le langage que BGGA dont la syntaxe est assez ardu a appréhender.
Les members littéral serait bien utile, mais du coup ce serait bien que les classes Field et Method soit Generics, afin de générer du code typesafe du style :
a++
PS : Merci pour l’erreur c’est corrigé
Encore un article intéressant sur les closures. Merci.
J’étais plutôt intéressé par la proposition BGGA pour les structures de contrôle. Mais combinée avec la proposition JCA, elle deviens vraiment intéressante. J’apprécie particulièrement la syntaxe qui me semble mieux s’intégrer avec la syntaxe actuelle.
Les members littéral me semblent aller aussi avec les proposition que j’avais pu lire à propos des bindings.
Au final j’adore cette proposition.
J’ai noté une petite erreur sur le 2eme encadré de code des structures de contrôle tu utilise code.invoke() alors qu’il faudrait block.invoke()