Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a difference between partial application and returning a function?

In terms of under the hood: stack/heap allocation, garbage collection, resources and performance, what is the difference between the following three:

def Do1(a:String) = { (b:String) => { println(a,b) }}
def Do2(a:String)(b:String) = { println(a,b) }
def Do3(a:String, b:String) = { println(a,b) }

Do1("a")("b")
Do2("a")("b")
(Do3("a", _:String))("b")

Except the obvious surface differences in declaration about how much arguments each takes and returns

like image 286
Alex Avatar asked Oct 30 '22 20:10

Alex


1 Answers

Decompiling the following class (note the additional call to Do2 compared to your question):

class Test {
  def Do1(a: String) = { (b: String) => { println(a, b) } }
  def Do2(a: String)(b: String) = { println(a, b) }
  def Do3(a: String, b: String) = { println(a, b) }

  Do1("a")("b")
  Do2("a")("b")
  (Do2("a") _)("b")
  (Do3("a", _: String))("b")
}

yields this pure Java code:

public class Test {
    public Function1<String, BoxedUnit> Do1(final String a) {
        new AbstractFunction1() {
            public final void apply(String b) {
                Predef..MODULE$.println(new Tuple2(a, b));
            }
        };
    }

    public void Do2(String a, String b) {
        Predef..MODULE$.println(new Tuple2(a, b));
    }

    public void Do3(String a, String b) {
        Predef..MODULE$.println(new Tuple2(a, b));
    }

    public Test() {
        Do1("a").apply("b");
        Do2("a", "b");
        new AbstractFunction1() {
            public final void apply(String b) {
                Test.this.Do2("a", b);
            }
        }.apply("b");
        new AbstractFunction1() {
            public final void apply(String x$1) {
                Test.this.Do3("a", x$1);
            }
        }.apply("b");
    }
}

(this code doesn't compile, but it suffices for analysis)


Let's look at it part by part (Scala & Java in each listing):

def Do1(a: String) = { (b: String) => { println(a, b) } }

public Function1<String, BoxedUnit> Do1(final String a) {
    new AbstractFunction1() {
        public final void apply(String b) {
            Predef.MODULE$.println(new Tuple2(a, b));
        }
    };
}

No matter how Do1 is called, a new Function object is created.


def Do2(a: String)(b: String) = { println(a, b) }

public void Do2(String a, String b) {
    Predef.MODULE$.println(new Tuple2(a, b));
}

def Do3(a: String, b: String) = { println(a, b) }

public void Do3(String a, String b) {
    Predef.MODULE$.println(new Tuple2(a, b));
}

Do2 and Do3 compile down to the same bytecode. The difference is exclusively in the @ScalaSignature annotation.


Do1("a")("b")

Do1("a").apply("b");

Do1 is straight-forward: the returned function is immediately applied.

Do2("a")("b")

Do2("a", "b");

With Do2, the compiler sees that this is not a partial application, and compiles it to a single method invocation.


(Do2("a") _)("b")

new AbstractFunction1() {
    public final void apply(String b) {
        Test.this.Do2("a", b);
    }
}.apply("b");

(Do3("a", _: String))("b")

new AbstractFunction1() {
    public final void apply(String x$1) {
        Test.this.Do3("a", x$1);
    }
}.apply("b");

Here, Do2 and Do3 are first partially applied, then the returned functions are immediately applied.


Conclusion:

I would say that Do2 and Do3 are mostly equivalent in the generated bytecode. A full application results in a simple, cheap method call. Partial application generates anonymous Function classes at the caller. What variant you use depends mostly on what intent you're trying to communicate.

Do1 always creates an immediate function object, but does so in the called code. If you expect to do partial applications of the function a lot, the using this variant will reduce your code size, and maybe trigger the JIT-Compiler earlier, because the same code is called more often. Full application will be slower, at least before the JIT-Compiler inlines and subsequently eliminates object creations at individual call sites. I'm not an expert on this, so I don't know whether you can expect that kind of optimization. My best guess would be that you can, for pure functions.

like image 190
Silly Freak Avatar answered Nov 09 '22 12:11

Silly Freak