lambda

En los últimos años, las expresiones Lambda han arrasado con el mundo de la programación. La mayoría de los lenguajes modernos las han adoptado como una parte fundamental de la programación funcional. Asimismo, se han convertido en una pieza clave en aquellos lenguajes basados en la JVM, como Scala, Groovy y Clojure. Y ahora, por fin, Java 8 se suma a la fiesta.

Algo interesante de las expresiones Lambda es que, desde el punto de vista de la JVM, son absolutamente invisibles. La máquina virtual no tiene ni idea de qué es una función anónima o una expresión Lambda. Lo único que reconoce es bytecode, una especificación estricta de la Orientación a Objetos. Queda en manos de los creadores del lenguaje y de su compilador trabajar dentro de esos marcos para crear elementos nuevos y más avanzados del lenguaje.

La primera vez que nos topamos con esto fue cuando estábamos agregando el soporte para Scala a Takipi y tuvimos que bucear en las aguas profundas del compilador de Scala. Con Java 8 a la vuelta de la esquina, pensé que sería muy interesante ver cómo implementaban las expresiones Lambda los compiladores de Java y Scala. Los resultados fueron bastante inesperados.

Para comenzar, tomé una expresión Lambda simple que convierte una lista de Strings en una lista de sus longitudes

En Java –


List names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

 

En Scala –


val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

No te creas que es tan simple: detrás de bambalinas están ocurriendo asuntos más complejos.

Comencemos con Scala

SCalaLam (1)

The Code

El código

Usé javap para visualizar los contenidos –en bytecode- del .class generado por el compilador de Scala. Veamos el bytecode resultante (que es lo que la JVM, al fin y al cabo, ejecutará).


// this loads the names var into the stack (the JVM thinks
// of it as variable #2).
// It’s going to stay there for a while till it gets used
// by the <em>.map</em> function.

aload_2

A continuación, las cosas se ponen más interesantes: una nueva instancia de la clase sintética generada por el compilador es creada e inicializada. Este es el objeto que, desde el punto de vista de la JVM, retiene al método Lambda. Resulta gracioso que, mientras que Lambda está definida como parte integral de nuestro método, en realidad vive totalmente por fuera de nuestra clase.

new myLambdas/Lambda1$$anonfun$1 //instantiate the Lambda object
dup //put it into the stack again

// finally, invoke the c’tor. Remember - it’s just a plain object
// from the JVM’s perspective.
invokespecial myLambdas/Lambda1$$anonfun$1/()V

// these two (long) lines loads the immutable.List CanBuildFrom factory
// which will create the new list. This factory pattern is a part of
// Scala’s collections architecture
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;

// Now we have on the stack the Lambda object and the factory.
// The next phase is to call the .<em>map</em>() function.
// If you remember, we loaded the <em>names</em> var onto
// the stack in the beginning. Now it’ll gets used as the
// this for the .<em>map</em>() call, which will also
// accept the Lambda object and the factory to produce the
// new list of lengths.

invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

Pero, ¡un momento! ¿Qué ocurre dentro del objeto Lambda?

El objeto Lambda

La clase Lambda deriva de scala.runtime.AbstractFunction1. A través de esto, la función map() puede hacer una invocación polimórfica al método sobreescrito apply(), cuyo código aparece más abajo.

// this code loads this and the target object on which to act,
// checks that it’s a String, and then calls another apply overload
// to do the actual work and boxes its return value.
aload_0 //load this
aload_1 //load the string arg
checkcast java/lang/String //make sure it’s a String - we got an Object

// call another apply() method in the synthetic class
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I

//box the result
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

El verdadero código para realizar la operación .length() está anidado en ese método apply adicional, que simplemente devuelve la longitud del String, según lo esperado.

¡Uff!… ¡un largo camino para llegar hasta aquí 🙂

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

Para una línea tan simple como la que originalmente escribimos, más arriba, se genera mucho bytecode: una clase adicional y una cantidad de métodos. Por supuesto, no es que esto nos aleje de la mente la idea de emplear Lambdas (estamos escribiendo en Scala, no en C). Pero sí es válido para mostrar la complejidad existente detrás de esos constructos. ¡Piensa por un momento en la montaña de código encerrada en la compilación de complejas cadenas de expresiones Lambda!

Yo esperaba que Java 8 implementara esto de la misma forma, pero me llevé una gran sorpresa al ver que ha tomado un rumbo completamente distinto.

Java 8 – Un nuevo abordaje

El bytecode aquí es más pequeño pero realiza algo más bien sorpresivo. Comienza de forma muy simple, cargando la var names e invocando su método .stream(), pero a esa altura, realiza algo muy elegante. En vez de crear un nuevo objeto que envuelva a la función Lambda, usa la instrucción invokeDynamic, agregada en Java 7, para enlazar dinámicamente el sitio de esta llamada con la función Lambda.

aload_1 //load the names var

// call its stream() func
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;

//invokeDynamic magic!
invokedynamic #0:apply:()Ljava/util/function/Function;

//call the map() func
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

La magia de InvokeDynamic. Esta instrucción de la JVM fue añadida en Java 7 para hacer menos estricta a la JVM, y permitir a los lenguajes dinámicos la asociación de símbolos en el tiempo de ejecución, en vez de hacer todas las vinculaciones de forma estática cuando el código ya fue compilado por la JVM.

Vinculación dinámica. Si miras la instrucción invokedynamic, notarás que no hay ninguna referencia de la función Lambda (llamada lambda$0). La razón recae en la forma en que invokedynamic está diseñada (lo cual merece todo un artículo aparte), pero en resumen, la razón está en el nombre y signatura de la Lambda, que en este caso es:

// a function named lamda$0 that gets a String and returns an Integer
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

 

Estas están almacenadas en una entrada en una tabla separada en el .class en que el parámetro #0 pasó los puntos de instrucción. Esta nueva tabla verdaderamente cambió la estructura de especificación del bytecode por primera vez en unos cuantos (y buenos) años, forzándonos a adaptar también el motor de análisis de errores de Takipi a ello.

El código Lambda

Este es el verdadero código de la expresión Lambda. Es muy similar: simplemente carga el parámetro de String, llama a length() y arroja el resultado. Verás que fue compilada como una función static para no tener que pasar un objeto this adicional a ella, como vimos en Scala.

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

Esta es otra ventaja del abordaje con invokedynamic, ya que el mismo permite invocar al método de una manera que es polimórfica desde la perspectiva de la función .map(), pero sin necesidad de ubicar un objeto wrapper o un método virtual de sobreescritura. ¡Qué interesante!

Resumen. Es fascinante ver cómo ahora Java, el más “estricto” de los lenguajes modernos está utilizando vinculación dinámica para manejar sus nuevas expresiones Lambda. Este es, además, un abordaje sumamente eficiente, ya que no son necesarias la carga ni la compilación de ninguna clase adicional: el método Lambda es simplemente un método privado en nuestra clase.
Java 8 ha realizado un trabajo verdaderamente elegante al utilizar una tecnología introducida en Java 7 para implementar las expresiones Lambda de una forma que resulta sumamente simple y directa. Resulta un placer ver cómo hasta una niña “venerable” como Java todavía puede enseñarnos algunos nuevos trucos a todos 🙂

fredForBlog

Takipi detects all your exceptions and errors and tells you why they happen. Even across multiple threads and machines. Installs in 1min. Less than 2% overhead – Try it free

Duke-T-shirt

Java developers – Deploy 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.