Compiling Lambda Expressions: Scala vs. Java 8
Lambda expressions have taken the programming world by storm in the last few years. Most modern languages have adopted them as a fundamental part of functional programming. JVM based languages such as Scala, Groovy and Clojure have integrated them as key part of the language. And now, Java 8 is (finally) joining in on the fun.
What’s interesting about Lambda expressions is that from the JVM’s perspective they’re completely invisible. It has no notion of what an anonymous function or a Lambda expression is. It only knows bytecode which is a strict OO specification. It’s up to the makers of the language and its compiler to work within these constraints to create newer, more advanced language elements.
We first encountered this when we were working on adding Scala support to Takipi and had to dive deep into the Scala compiler. With Java 8 right around the corner, I thought it would be interesting to see how the Scala and Java compilers implement Lambda expressions. The results were pretty surprising.
To get things going I took a simple Lambda expression that converts a list of Strings to a list of their lengths.
In Java -
List names = Arrays.asList("1", "2", "3"); Stream lengths = names.stream().map(name -> name.length());
In Scala -
val names = List("1", "2", "3") val lengths = names.map(name => name.length)
Don’t be tricked its simplicity - behind the scenes some complex stuff is going on.
Let’s start with Scala
I used javap to view the bytecode contents of the .class produced by the Scala compiler. Let’s look at the result bytecode (this is what the JVM will actually execute).
// 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 .map function. aload_2
Next, things gets more interesting - a new instance of a synthetic class generated by the compiler is created and initialized. This is the object that from the JVM’s perspective holds the Lambda method. It’s funny that while the Lambda is defined as an integral part of our method, in reality it lives completely outside of our class.
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 .map() function. // If you remember, we loaded the names var onto // the stack in the beginning. Now it’ll gets used as the // this for the .map() 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;
But hold on - what’s going on inside that Lambda object?
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)