2015-08-25 16 views
5

In Bezug auf dem unter der Haube: Stapel/Heapzuordnung, Müllabfuhr, Ressourcen und Leistung, was ist der Unterschied zwischen den folgenden drei:Gibt es einen Unterschied zwischen partieller Anwendung und Rückgabe einer Funktion?

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") 

Außer den offensichtlichen Oberflächenunterschiede Erklärung darüber, wie viele Argumente jeder nimmt und gibt

+0

Nun, das hat nichts mit Currying zu tun. In jedem Fall ging es um die Frage unter den Motorhauben. – Alex

+0

Entschuldigung, ich habe den wichtigen Teil Ihrer Frage übersehen: wie es sich auf die Speicherzuweisung bezieht. Ich denke immer noch, dass es eine Menge relevanter Informationen in der verwandten Frage gibt, aber es ist kein Duplikat, du hast Recht. –

+1

Ich würde auf einer Bytecode-Ebene schätzen, dass 'Do2' und' Do3' gleich sind. Ihr Aufruf an 'Do2' ist wahrscheinlich ein einfacher Methodenaufruf, während ich erwarten würde, dass der 'Do3'-Aufruf ein Zwischenfunktionsobjekt erzeugt. 'Do1' sollte das trotzdem tun. Vielleicht könnte jetzt jemand mit mehr Zeit einen 'javap' machen und es als Antwort aufschreiben. –

Antwort

2

Decompiling die folgende Klasse (beachten Sie den zusätzlichen Aufruf Do2 im Vergleich zu Ihrer Frage):

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") 
} 

ergibt diese reine 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"); 
    } 
} 

(dieser Code nicht kompiliert, sondern es reicht für die Analyse)


ist es Teil für Teil (Scala & Java in jeder Auflistung) Schauen wir uns:

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)); 
     } 
    }; 
} 

Unabhängig davon, wie Do1 aufgerufen wird, wird ein neues Funktionsobjekt erstellt.


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 und Do3 auf den gleichen Bytecode kompiliert unten. Der Unterschied liegt ausschließlich in der @ScalaSignature Annotation.


Do1("a")("b") 

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

Do1 ist geradlinig: die zurückgegebene Funktion sofort angewendet wird.

Do2("a")("b") 

Do2("a", "b"); 

Mit Do2 sieht der Compiler, dass dies nicht eine Teilanmeldung ist, und stellt es auf einen einzigen Methodenaufruf.


(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"); 

Hier Do2 und Do3 sind zunächst teilweise angewendet wird, dann werden die zurückgegebenen Funktionen sofort angewendet.


Fazit:

Ich würde sagen, dass Do2 und Do3 in der erzeugten Bytecode meist gleichwertig sind. Eine vollständige Anwendung führt zu einem einfachen, kostengünstigen Methodenaufruf. Teilapplikation erzeugt beim Aufrufer anonyme Funktionsklassen. Welche Variante Sie verwenden, hängt hauptsächlich davon ab, welche Absicht Sie kommunizieren möchten.

Do1 erstellt immer ein sofortiges Funktionsobjekt, tut dies jedoch im aufgerufenen Code. Wenn Sie erwarten, dass Sie die Funktion teilweise ausführen, reduziert die Verwendung dieser Variante Ihre Codegröße und löst möglicherweise den JIT-Compiler früher aus, da der gleiche Code häufiger aufgerufen wird.Die vollständige Anwendung wird zumindest vor den Inline-Anweisungen des JIT-Compilers langsamer und beseitigt anschließend die Erstellung von Objekten an einzelnen Call-Sites. Ich bin kein Experte darin, also weiß ich nicht, ob Sie diese Art der Optimierung erwarten können. Meine beste Vermutung wäre, dass Sie für reine Funktionen können.

+0

Vielen Dank für die detaillierte Analyse und vor allem den Abschluss. Schätzen Sie die Zeit und den Aufwand. – Alex

+0

@Alex Gern geschehen! Es tut uns leid, dass du den Punkt deiner Frage zuerst verpasst hast. Es zu beantworten war sehr interessant! –