2015-08-25 19 views
5

In termini di sotto del cofano: assegnazione Pila/mucchio, raccolta rifiuti, risorse e prestazioni, qual è la differenza tra i seguenti tre:C'è una differenza tra l'applicazione parziale e il ritorno di una funzione?

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

Tranne le differenze superficiali evidenti dichiarazione su quanto argomenti ognuno prende e restituisce

+0

Bene, questo non ha nulla a che fare con il currying. In ogni caso, la domanda era circa sotto i cofani. – Alex

+0

Scusa, ho trascurato una parte importante della tua domanda: in che modo si riferisce all'assegnazione della memoria. Penso ancora che ci siano molte informazioni rilevanti nella domanda correlata, ma non è un duplicato, hai ragione. –

+1

Direi su un livello di bytecode che "Do2" e "Do3" sono gli stessi. La tua chiamata a 'Do2' è probabilmente una semplice chiamata di metodo, mentre mi aspetterei che la chiamata' Do3' per creare un oggetto funzione intermedia. "Do1" dovrebbe farlo comunque. Forse qualcuno con più tempo in questo momento potrebbe fare un 'javap' e scriverlo come risposta. –

risposta

2

decompilazione la classe seguente (si noti la chiamata supplementare per Do2 rispetto alla tua domanda):

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

cede questo puro codice Java:

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

(questo codice non viene compilato, ma è sufficiente per l'analisi)


Vediamo che parte per parte (Scala & Java in ogni elenco):

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

Indipendentemente da come viene chiamato Do1, viene creato un nuovo oggetto funzione.


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 e Do3 compilare verso il basso per lo stesso bytecode. La differenza è esclusivamente nell'annotazione @ScalaSignature.


Do1("a")("b") 

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

Do1 è diretta: la funzione restituita viene applicato immediatamente.

Do2("a")("b") 

Do2("a", "b"); 

Con Do2, il compilatore vede che questa non è una parziale applicazione, e compila un metodo singola chiamata.


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

Qui, Do2 e Do3 sono prima parzialmente applicate, allora le funzioni restituiti vengono immediatamente applicate.


Conclusione:

direi che Do2 e Do3 sono per lo più equivalente nel bytecode generato. Un'applicazione completa produce una chiamata al metodo semplice ed economica. L'applicazione parziale genera classi di funzioni anonime al chiamante. Quale variante tu usi dipende principalmente dall'intento che stai cercando di comunicare.

Do1 crea sempre un oggetto funzione immediato, ma lo fa nel codice chiamato. Se si prevede di eseguire parziali applicazioni della funzione, l'utilizzo di questa variante ridurrà le dimensioni del codice e potrebbe far scattare il compilatore JIT in precedenza, poiché lo stesso codice viene chiamato più spesso.La piena applicazione sarà più lenta, almeno prima dell'integrazione del compilatore JIT e successivamente eliminerà le creazioni di oggetti nei singoli siti di chiamata. Non sono un esperto in questo, quindi non so se puoi aspettarti questo tipo di ottimizzazione. La mia ipotesi migliore sarebbe che tu possa, per le funzioni pure.

+0

Grazie mille per l'analisi dettagliata e soprattutto per la conclusione. Apprezzo il tempo e lo sforzo. – Alex

+0

@Alex Prego! Scusa ancora per aver perso il punto della tua domanda all'inizio. La risposta è stata molto interessante! –