Un ch'ti peu de lambda
des lambdas à l'ombre du beffroi
Rémi Forax
Décembre 2012
Moi
MCF à l'université Paris Est Marne-La-Vallée
Joue avec Java depuis trop longtemp pour
l'avouer
Créateur de langage dynamique ou pas
Expert pour les JSR 292 (invokedynamic) et
JSR 335 (lambda)
Contributeur OpenJDK, ASM, Tatoo, PHP.reboot, JDart,
etc...
Les lambdaaaaahs
Pourquoi
par ce que Java c'était trop simple ??
Comment
on va tout casser Java ??
Sous le capot
surtout ne pas toucher à la VM ??
Problème des classes anonymes
Une classe anonyme est pas super adaptée
visuellement
Verbeux, rapport signal/bruit pas satisfaisant
sémantiquement
On veut envoyer une expression, créont une classe ...
performance
création d'un objet à chaque appel
+1 classe sur le disque
+1 instance de java.lang.Class + métadata en mémoire
Comment font les autres langages ?
Lisp/Clojure closure's
Ruby block
Groovy Closure
Scala/C# lambda
Javascript/Python anonymous function
Python comprehension/C# Linq
Pourquoi introduire les lambdas en Java ?
les classes anonymes couvrent déjà le besoin ??
L'excuse multicore
On a plein de coeurs et on sait pas quoi en faire !
Si une partie du code est transformable en objet
On peut distribuer / paralleliser l'exécution
Presque magique
Enfin, si on a un effet de bord, on est mort !
Avec une lambda ...
private static File[] subDirectories(File directory) {
FileFilter filter = (File path) -> path.isDirectory();
return file.listFiles(filter);
}
public static void main(String[] args) {
File file = new File(".");
for(File dir: subDirectories(file)) {
System.out.println(dir);
}
}
Syntaxe pour les expressions
sans paramètre
() -> System.out.println("welcome to the land of shtis")
avec un paramètre (inférence)
employee -> employee.isManager()
avec plusieurs paramètres
en déclarant les types
(int x, int y) -> x == y
sans déclarer les types (inférence)
(x, y) -> x == y
Syntaxe pour les instructions
sans paramètre
() -> {
System.out.println("welcome to the land of shtis");
}
avec un paramètre (inférence)
employee -> {
return employee.isManager();
}
avec plusieurs paramètres (inférence)
(index1, index2) -> {
list.set(index1, list.get(index2));
}
Sémantique
Typé par une functional interface (ex SAM)
Runnable, Callable, Filter, Function, ...
Une lambda n'est pas un objet
“this” représente la classe courante pas la lambda
Une lambda est convertissable en un objet qui
implante une functional interface
Method Reference
Raccourçi si la lambda délègue juste à une
méthode
On utilise :: rien que pour embéter les C++eux
Si la méthode est surchargée, l'inférence
utilise le target type pour trouver les types des
paramétres
Et les collections
Avoir des lambdas c'est bien, mais sans
support au niveau des APIs ...
Nouvelle interface java.util.Stream
Sequentielle ou parallele
Operations
intermédiaire
filter, map, sorted, distinct, flatMap ...
terminales
reduce, forEach, into, findFirst ...
Avec tout dans le main()
La méthode subdirectories() sert pas vraiment !
public static void main(String[] args) {
File directory = new File(".");
Arrays.stream(directory.listFiles()).
filter(File::isDirectory).
forEach(dir -> System.out.println(dir));
}
Capturer la valeur de variables locales
Et si je veux tous les sous-répertoires dont le nom
commence par args[0]
public static void main(String[] args) {
File directory = new File(".");
String name = args[0];
Arrays.stream(directory.listFiles()).
filter(File::isDirectory).
filter(dir -> dir.getName().startsWith(name).
forEach(dir -> System.out.println(dir));
}
Une lambda peut capturer les valeurs des variables locales
(comme avec une classe anonyme)
Pipeline d'élements
Arrays.asStream(array)
collection.stream() filter filter block
iterator.stream() false false
On pousse les élements à travers le pipeline
Arrays.stream(directory.listFiles()).
filter(File::isDirectory).
filter(dir -> dir.getName().startsWith(name).
forEach(dir -> System.out.println(dir));
Method reference sur une instance
Il est possible de créer une méthode réference sur une
instance
public static void main(String[] args) {
File directory = new File(".");
String name = args[0];
Arrays.stream(directory.listFiles()).
filter(File::isDirectory).
filter(path -> path.getName().startsWith(name).
forEach(System.out::println);
}
En résumé
Java permet de définir des lambdas et des
références sur des méthodes
Une lambda est une expression ou une fonction
anonyme que l'on peut convertir en un objet
pour envoyer à une méthode
L'API des collections est mis à jour pour
supporter les lambdas
Changements pour Java
Gros problèmes
Inférence déjà existante pas assez puissante
Interface pas extensible
Et des petits +
Effectively final
Plus besoin de déclarer les variables locales utilisée dans les
lambdas classes anonymes final
Eviter les garbages classes
Mettre les méthodes statiques public/private dans les interfaces
Améliorer l'inférence
Java 5/7 infére
les variables de type des méthodes
List<String> list =Array.asList("foo", "bar")
List<String> list = Collections.emptyList();
les instantiations de types paramétrés
List<String> list = new ArrayList<>();
=> mais f(Collections.emptyList()) marche pas !
Inference avec Java 8
Inference pour les types des lambdas
donc de gauche à droite comme les <>
Inference pour appel de méthode/instantiation
diamond (JEP 101)
si dans un appel de méthode
foo(new ArrayList<>()); // ok
avec propagation
String s = Collections.emptyList().get(0); // ok
Extensibilité des interfaces
On veux obtenir un Stream à partir d'une
Collection
collection.stream()
On ne peut pas ajouter une méthode dans une
interface !
Solution académique: traits (!= Scala traits)
Trait
Un type contenant des méthodes abstraites et
des méthodes concrètes (mais pas de champs)
Ajouter une méthode si l'on fournit l'implantation
ne casse pas la compatibilité
idée: et si on pouvait mettre du code dans une
interface
Default method
Permet de fournir un code par défaut qui est utiliser si il n'en
n'existe pas
interface Iterator<T> {
public boolean hasNext();
public T next();
public default void remove() {
throw new UnsupportedOperationException();
}
public default void forEach(Block<? super T> block) {
while(hasNext()) {
block.accept(it.next());
}
}
}
Sémantique
La méthode par défaut n'est utilisée que
“par défaut”
interface A {
default void m() { ... }
}
class B implements A{
void m() { ... } // pas besoin de A::m !
}
Héritage mutiple ??
interface A {
default void m() { ... }
}
interface B {
default void m() { ... }
}
class C implements A, B {
// compile pas, faut choisir A::m ou B::m
}
Héritage mutiple
interface A {
default void m() { ... }
}
interface B {
default void m() { ... }
}
class C implements A, B {
// on doit fournir un code pour m()
public void m() {
A.super.m(); // on appel m de A
B.super.m(); // on appel m de B
}
}
Au menu ...
Comment les méthodes par défaut
fonctionnent ?
Comment les lambdas sont compilés ?
Comment les lambdas sont optimisées par la
VM ?
Méthode par défaut
Le compilo ne peut rien faire !
Sinon on doit recompiler toutes les libraries
Doit être fait par la VM
mais
● les règles de redéfinition (override) dépendent des generics
● Les générics sont un artifact à la compile pas connu à
l'exécution (erasure)
La VM doit savoir lire les signatures des generics
Méthode par défaut et erasure
interface Foo<T> {
default T getFoo() { return ...; }
}
interface Bar {
String getFoo();
}
class A implements Bar, Foo<String> {
}
Méthode par défaut et erasure
interface Foo<T> {
default ObjectT getFoo() { return ...; }
}
interface Bar {
String getFoo();
}
class A implements Bar, Foo<String> {
}
la VM doit générer deux méthodes
Object getFoo() et String getFoo()
Méthode par défaut et erasure
interface Foo<T> {
default ObjectT getFoo() { return ...; }
}
interface Bar {
String getFoo();
}
class A implements Bar, Foo<String> {
Object getFoo() {
return getFoo(); // appel String getFoo()
}
String getFoo() {
return ...; // recopie le bytecode de Foo::getFoo
}
}
Compiler une lambda naïvement
On créé une méthode synthetic pour le corps de la lambda
On crée une classe anonyme lors de la convertion vers la
functional interface
iterator.forEach(dir -> System.out.println(dir));
devient
iterator.forEach(new Block<File>() {
public void accept(File dir) {
return lambda$1(dir);
}
});
static void lambda$1(File dir) {
System.out.println(dir);
}
Lambda objet constant ?
A l'exécution, il y a deux sortes de lambdas
Les lambdas qui ne captures pas de variable ou les
méthodes référence sur une classe
● path -> path.isDirectory
● File::isDirectory
Celles qui capture des variables ou les méthode
référence sur des instances
● path -> path.getName().startsWith(args[0)
● System.out::println
Compiler vers une classe anonyme ?
Et on se récupère tous les problèmes de perf
des classes anonymes
De plus, si une lambda ne capture pas de
variable, on pourrait au moins la crée que une
seule fois
Mais si on utilise un champ static final,
l'initialisation à lieu même si on ne l'utilise pas
Invokedynamic to rule them all
On veut un mécanisme qui délai l'initialisation au
premier accès
invokedynamic
On veut un mécanisme qui permet d'indiquer que le
résultat d'un calcul est constant
invokedynamic
On veut un pointeur de fonction pour éviter la
création des classe anonymes
java.lang.invoke.MethodHandle
Compiler une lambda
iterator.forEach(dir -> System.out.println(dir));
devient
iterator.forEach(invokedynamic bootstrap [lambda$1]);
static void lambda$1(File dir) {
System.out.println(dir);
}
CallSite bootstrap(Lookup lookup, String name,
MethodType type, MethodHandle mh) {
if (type.parameterCount() == 0) {
return new ConstantCallSite(proxy(mh));
}
return ...
}
Lambda Proxy
Instance d'une classe qui contient le pointeur de
fonction (MethodHandle) vers la lambda à
exécuter
Il n'y a besoin que d'une classe proxy par
functional interface
Le proxy est généré dynamiquement par la VM,
pas forcément besoin de bytecode associé
Lambda Proxy en pseudo code
Le code Java correspondant est à peu près :
public class Proxy$Block implements Block {
private final @Stable MethodHandle mh;
public void accept(Object o) {
mh.invokeExact(o);
}
}
invokeExact() est un pointer check + un appel à un
pointeur de fonction
Optimizations lors de l'appel
Avoir 1 seul classe pour 1 interface permet à la VM de
remplacer l'appel à l'interface par le code de la classe
(Class Hierarchy Analysis)
interface Iterator<T> {
...
public default void forEach(Block<? super T> block) {
while(hasNext()) {
block.accept(it.next());
}
}
}
ici, block est toujours une instance de Proxy$Block
Optimizations lors de l'appel
Si un objet constant dans une boucle, il est sortie de la boucle
interface Iterator<T> {
...
public default void forEach(Block<? super T> block) {
while(hasNext()) {
mh.invokeExact(it.next());
}
}
}
Dans le cas d'un method handle, il faut inliner le code
référencé par le pointer de fonction (fairy tale mode)
dans le monde merveilleux
des licornes
On guarde la boucle avec un test sur le method handle
(comme pour l'On Stack Replacement)
interface Iterator<T> {
...
public default void forEach(Block<? super T> block) {
if (block.mh == lambda$1) {
while(hasNext()) {
System.out.println(it.next());
}
}
else deopt();
}
}
et comme block est pris en paramètre, l'idée est de spécialiser le code à
l'endroit de l'appel ce qui permet de ne pas faire le test
Le léger hic
Le code de la VM qui crée le lambda proxy et qui l'optimise
est pas prêt
Le plan est prêt depuis longtemps, c'est la réalisation qui prend
du temps
On doit quand même livrer un truc pour le JDK8
Solution temporaire: la méthode de bootstrap génère une
classe anonyme dynamiquement en utilisant ASM
On implantera les lambda proxies dans une update du jdk8
En résumé
La première version des lambdas sera pas la
plus optimisée
L'API dans java.util doit être fini pour fin
janvier
La spec de l'inférence pas fini même si ça se
précise
Bref on est grâve à la bourre
Si vous avez du temps libre
et même si vous n'en avez pas !
Downloader la dernière version du jdk8 avec les
lambdas
http://jdk8.java.net/lambda/
Tester l'API, jouer avec, remonter tous les bugs
lambda-dev@openjdk.java.net