lambda

Les expressions lambda ont bouleversé le monde de la programmation au cours des dernières années. La plupart des langages modernes les ont adoptés comme un élément fondamental de la programmation fonctionnelle. Les langages basés JVM tels que Scala, Groovy et Clojure les ont intégrés dans un cadre essentiel du langage. Et maintenant, Java 8 s’est (enfin) joint à la fête.

Ce qui est intéressant, c’est que les expressions lambda, du point de vue JVM, sont totalement invisibles. Il n’a aucune idée de ce qu’est une fonction anonyme ou une expression lambda. Il ne connaît que le bytecode qui est une spécification du OO strict. C’est aux décideurs du langage et à ses compilateurs de travailler avec ces contraintes pour créer de nouveaux éléments de langage plus perfectionnés.

Nous avons rencontré cela en premier lorsque nous avons travaillé sur l’ajout du support Scala à Takipi et on a dû plonger dans le compilateur Scala. Avec Java 8 juste autour du coin, j’ai pensé qu’il serait intéressant de voir comment les compilateurs Java et Scala et mettent en œuvre des expressions lambda. Les résultats ont été assez surprenants.

Pour faire avancer les choses, j’ai pris une expression lambda simple qui convertit une liste de chaînes à une liste de leurs longueurs.

En Java –

Listes de noms = Arrays.asList("1", "2", "3");
Longueurs totales = names.stream().map(name -> name.length());

En Scala –

val noms = List("1", "2", "3")
val longueurs = names.map(nom => nom.longeur)

Ne vous fiez pas sa simplicité – derrière, il’ya des trucs complexes qui se passent.

Commençons avec Scala

SCalaLam (1)

Le Code

J’ai utilisé javap pour afficher le contenu de bytecode du .class produit par le compilateur Scala. Regardons le résultat bytecode (c’est ce que JVM exécutera réellement).

// ceci charge les noms var dans la pile (JVM estime
// que c’est une variable #2).
// Il va y rester pendant un certain temps jusqu'à ce qu'il s'habitue
// de la fonction  .map.
aload_2

Ensuite, les choses deviennent plus intéressantes – une nouvelle instance d’une classe synthétique générée par le compilateur est créée et initialisée. C’est l’objet du point de vue JVM qui détient la méthode Lambda. C’est drôle que tout Lambda est définie comme une partie intégrante de notre méthode, en réalité, elle vit totalement en dehors de notre classe.

new myLambdas/Lambda1$$anonfun$1 //instancier l'objet Lambda
dup //mettre dans la pile à nouveau
// invoquer c'tor. Rappelez-vous - c'est juste un objet ordinaire
// du point de vue JVM.
invokespecial myLambdas/Lambda1$$anonfun$1/()V
// ces deux (longues) lignes chargent l’immutable.List CanBuild
// qui créera la nouvelle liste. Ce modèle de fabrique est une partie de
// l’architecture de collections Scala
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;
// Maintenant, nous avons sur la pile, l'objet Lambda et l'usine.
// La prochaine étape est d’appeler la fonction .map().
//Si vous vous rappelez, nous avons chargé les var names sur
// la pile au début. Maintenant, il va se servir de
// ceci pour l’appel .map(),qui va également
// accepter l'objet Lambda et la fabrique pour produire la
// nouvelle liste de longeurs.
invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

Mais attendez- qu’est ce qui se passe à l’intérieur de cet objet lambda?
L’objet Lambda
La classe Lambda est dérivée de scala.runtime.AbstractFunction1. A travers cette fonction map(), on peut invoquer d’une manière polymorphe la substitution apply() dont le code est ci-dessous –

// ce code charge ceci et l'objet cible sur lequel il agit,
// vérifie que c'est une chaîne, puis appelle une autre apply de surcharge
// pour faire le travail actuel et retourner une valeur.
aload_0 //charger ceci
aload_1 //charger la chaine arg
checkcast java/lang/String // s’assurer que c'est une chaîne - nous avons eu un Objet
// appel une autre méthode apply() dans la classe synthétique
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I
//retourner le résultat
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

Le code actuel pour effectuer l’opération .length() qui est nichée dans cette méthode et qui renvoie simplement la longueur de la chaîne que nous nous attendions.
Ouf ..on a fait un long chemin pour arriver ici:)

aload_1
invokevirtual java/lang/String/length()I
ireturn

Pour une ligne aussi simple que nous écris ci-dessus, beaucoup de bytecode sont générés – une classe supplémentaire et un tas de nouvelles méthodes. Bien-sûr, ceci n’est pas fait pour dissuader d’utiliser lambdas (nous programmons en Scala, non en C). C’est juste pour montrer la complexité derrière ces constructions. Il suffit de penser de la quantité et de la complexité du code lors de la compilation des chaînes sur lambda!

J’attendais Java 8 pour mettre en œuvre ceci de la même manière, mais j’étais très surpris qu’ils ont opté pour une autre approche complètement différente.

Java 8 – Une nouvelle approche

JavaLam (1)

Le bytecode ici est un peu plus court mais fait quelque chose d’assez surprenant. Il commence tout simplement en chargeant les var noms et les appelle méthode .stream(), mais il fait quelque chose de tout à fait élégante. Au lieu de créer un nouvel objet qui dissimule la fonction lambda, il utilise la nouvelle instruction invokeDynamic qui a été ajoutée dans Java 7 pour lier dynamiquement cet appel à la fonction réelle Lambda.

aload_1 //charger les var noms
//appeler ceci fonction stream()
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;
//invokeDynamic magic!
invokedynamic #0:apply:()Ljava/util/function/Function;
//appeler la fonction map()
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

InvokeDynamic magic Cette instruction JVM a été ajoutée dans Java 7 pour rendre JVM moins stricte, et permettre aux langages dynamiques de lier des symboles au moment de l’exécution, au lieu de faire le lien statique lorsque le code est compilé par JVM.

Liaison dynamique. Si vous regardez l’instruction invokedynamic, vous verrez qu’il n’y a pas de référence de la fonction réelle de Lambda (appelée lambda$0). La réponse réside dans la façon dont est conçue invokedynamic (ce qui en mérite un poste complet), mais une réponse courte est le nom et la signature de Lambda, qui dans notre cas est –

// une fonction nommée lamda$0 qui prend une Chaine et retourne un entier
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

sont stockées dans une entrée dans une table séparée dans .class avec le paramètre #0 qui est transmis à des points d’instructions. Ce nouveau tableau modifie la structure de la spécification bytecode pour la première fois après quelques bonnes années, ce qui nous oblige à adapter le moteur d’analyse d’erreurs de Takipi avec ce dernier.

Le code Lambda

Il s’agit du code pour l’expression réelle Lambda. C’est une approche unique qui charge le paramètre de chaîne, appelle lenght() et renvoi le résultat. Notez qu’il a été compilé en tant que fonction statique pour éviter de passer par un objet supplémentaire comme nous l’avons vu sur Scala.

aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn

C’est un autre avantage de l’approche invokedynamic, car elle nous permet d’invoquer la méthode d’une manière qui est polymorphe par rapport à la perspective de la fonction .map(), mais sans avoir à allouer un objet de couverture ou invoquer une méthode de substitution virtuelle. C’est vraiment génial!
Résumé. C’est fascinant de voir comment Java, le langage le plus «stricte» des langues moderne, utilise une liaison dynamique pour alimenter ses nouvelles expressions Lambda. C’est aussi une approche efficace, pas de chargement de classe supplémentaire et la compilation est nécessaire – la méthode Lambda est tout simplement une autre méthode privée dans notre classe.
Java 8 a vraiment fait un travail très élégant en utilisant une nouvelle technologie introduite dans Java 7 pour mettre en œuvre les expressions Lambda, ce qui représente une manière très simple. C’est vraiment agréable de voir que même une dame “vénérable” telle que Java peut nous enseigner à tous les nouveaux trucs 🙂

Duke-T-shirt

Java/Scala developers – Install Takipi now and get a free T-shirt

Java 8

Java 8 Exceptions have never been so beautiful – Try Takipi for Java 8

email
Tal is the CEO of OverOps. Tal has been designing scalable, real-time Java and C++ applications for the past 15 years. He still enjoys analyzing a good bug though, and instrumenting code. In his free time Tal plays Jazz drums.