In this blog, we will talk about eta expansion in Scala. How they behave under the hood before and after Scala version 2.12.
Scala has both methods and functions in Scala. We refer them interchangeably but there are situations where the difference matters.
When you see the bytecode generated by the Scala compiler for Test,
Notice, isEven method created using def, is a regular function but isEvn is an instance of the Function1 trait. Besides, that methods are not values but functions are.
Notice, when we print the value of x and isEvn both gave us the information of itself but when we tried to do with isEven method, the compiler throws an error because isEven is not a value.
We saw that when we create a method using val, Scala compiler created an instance of the Function1 trait.
There are series of traits from Function0, Function1 to Function22. You can not have the Function23 trait. And for every function value scala compiler creates an anonymous class.
Second isEvn implementation is the actual implementation done by Scala Compiler under the hood. Every FunctionN trait has an apply method where our implementation goes. When you compile code Test.scala and see Scala compiler creates two class files for Test i.e Test.class and Test$$anonfun$1.class. Test$$anonfun$1.class is anonymous class created for function in Scala.
Whatever we have talked about so far about FunctionN trait and anonymous class for every function values, this all happens before Scala version 2.12. But after Scala version 2.12, Scala Compiler does not create anonymous classes for every function. Instead, it uses factories to create dynamic classes on the fly. Jar becomes smaller and loads faster.
When you again compile the same file. You will notice there is only one class file i.e Test.class. Let see the bytecode generated,
Notice Compiler has smartly defined $anonfun$isEvn$1 as a static method under the hood so that it could be called without an object. And in Test() what is invokedynamic call?
invokedynamic is used to generate code at runtime. Basically, lambdas are not invoked using invokedynamic. Their object representation is created using invokedynamic, the actual invocation is a regular call either invokevirtual or invokeinterface or invokestatic.
Let’s back to our main aim of eta expansion. We have seen how Scala Compiler does automatic eta expansion. There is a situation where the compiler is not expecting a function so it won’t do automatic eta expansion.
For example, creating a method and trying to store it in Map
Here Compiler has thrown an error because we have method and compiler is not expecting a function so there is no automatic eta expansion.
So there are two ways of explicitly doing eta expansion as follows:
- Explicitly declare the type of value to be a function.
- Treat the method to be as partially applied function
Notice both ways have converted a method into a function. Now we try to store in Map, it won’t throw an error.
That’s all for now. We have seen that methods and functions are not the same things in Scala. How Scala compiler smartly does converts the method into a function using eta expansion. And techniques to do manually eta expansion if Scala compiler won’t do automatically.
Please feel free to suggest and comment.