Is there any specification of scala compilator that can explain that behaviour?
scala version: 2_10_6
code example
trait Service {
def process(s: String)
}
object ServiceImpl extends Service{
override def process(s: String): Unit = {
println(s)
}
}
object Register {
var serviceInst : Service = ServiceImpl
}
object Client1 {
def process1(l: List[String]): Unit ={
l.foreach(x => Register.serviceInst.process(x))
}
}
object Client2 {
def process1(l: List[String]): Unit ={
l.foreach(Register.serviceInst.process)
}
}
I assume that process1 and process2 should have the similar behaviour. However, after comilation / decom
public final class Client1$$anonfun$process$1$$anonfun$apply$1 extends AbstractFunction1<String, BoxedUnit> implements Serializable {
public static final long serialVersionUID = 0L;
public final void apply(final String x$1) {
Register$.MODULE$.serviceInst().process(x$1);
}
}
public static final class Client2$$anonfun$process$1 extends AbstractFunction1<String, BoxedUnit> implements Serializable {
public static final long serialVersionUID = 0L;
private final Service eta$0$1$1;
public final void apply(final String s) {
this.eta$0$1$1.process(s);
}
}
It's because Scala compiler performs eta-expansion on method given in Client2
, which works by generating Function
that calls process
directly on a concrete Service
instance.
Here is an example how these functions look like before they are turned into bytecode:
object Client1 {
def process1(l: List[String]): Unit = {
l.foreach(new Function1[String, Unit] {
def apply(x: String) = Register.serviceInst.process(x)
})
}
}
object Client2 {
def process1(l: List[String]): Unit = {
l.foreach(new Function1[String, Unit] {
val eta = Register.serviceInst
def apply(x: String) = eta.process(x)
})
}
}
It's become more interesting if we rewrite serviceInst
a bit:
object Register {
def serviceInst : Service = {
println("get service instance!!!")
ServiceImpl
}
}
And then execute:
Client1.process1(List("a","b"))
Client2.process1(List("a","b"))
Obviously results are different:
1.
get service instance!!!
a
get service instance!!!
b
res0: Unit = ()
2.
get service instance!!!
a
b
res1: Unit = ()
Explanation is behind parameter of foreach function:
Client1
contains function as below, that executes each invocation x => Register.serviceInst.process(x)
Client2
has function process
that's going to be executed, but firstly serviceInst
is about to be initialized.
The line below
l.foreach(x => Register.serviceInst.process(x))
is operationally equivalent to
l.foreach(Register.serviceInst.process)
The first one is called "point-ful style" while the second is "point-free style", or more specifically "eta-conversion", with the term "point" referring to the named argument which doesn't exist in the second case. They are two different concepts and thus compile differently. You can write code in point-free style and the Scala compiler is eta-expanding it internally, which is what you're seeing in the decompiled code for Client2
.
eta conversion is a term from Lambda Calculus. If the sole purpose of a lambda abstraction is to pass its argument to another function, then the lambda is redundant and can be stripped via eta conversion/reduction. Java's Lambda Expressions vs Method References is another example.