Call by Name vs Call by Value in Scala, Klarstellung erforderlich


239

So wie ich es verstehe, kann in Scala auch eine Funktion aufgerufen werden

  • nach Wert oder
  • namentlich

Wissen wir beispielsweise angesichts der folgenden Deklarationen, wie die Funktion aufgerufen wird?

Erklärung:

def  f (x:Int, y:Int) = x;

Anruf

f (1,2)
f (23+55,5)
f (12+3, 44*11)

Was sind die Regeln bitte?

Antworten:


538

In dem Beispiel, das Sie angegeben haben, wird nur Call-by-Value verwendet. Daher werde ich ein neues, einfacheres Beispiel geben, das den Unterschied zeigt.

Nehmen wir zunächst an, wir haben eine Funktion mit Nebeneffekt. Diese Funktion druckt etwas aus und gibt dann ein zurück Int.

def something() = {
  println("calling something")
  1 // return value
}

Jetzt definieren wir zwei Funktionen, die IntArgumente akzeptieren , die genau gleich sind, außer dass eine das Argument in einem Call-by-Value-Stil ( x: Int) und die andere in einem Call-by-Name-Stil ( x: => Int) verwendet.

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

Was passiert nun, wenn wir sie mit unserer Nebenwirkungsfunktion aufrufen?

scala> callByValue(something())
calling something
x1=1
x2=1

scala> callByName(something())
calling something
x1=1
calling something
x2=1

Sie sehen also, dass in der Call-by-Value-Version der Nebeneffekt des übergebenen Funktionsaufrufs ( something()) nur einmal aufgetreten ist. In der Call-by-Name-Version trat der Nebeneffekt jedoch zweimal auf.

Dies liegt daran, dass Call-by-Value-Funktionen den Wert des übergebenen Ausdrucks vor dem Aufrufen der Funktion berechnen und daher jedes Mal auf denselben Wert zugegriffen wird. Stattdessen berechnen Call-by-Name-Funktionen den Wert des übergebenen Ausdrucks bei jedem Zugriff neu.


296
Ich habe immer gedacht, dass diese Terminologie unnötig verwirrend ist. Eine Funktion kann mehrere Parameter haben, die sich in ihrem Call-by-Name- und Call-by-Value-Status unterscheiden. So ist es nicht , dass eine Funktion Call-by-Name ist oder Call-by-Wert, dann ist es , dass jeder seiner Parameter kann passieren -mit-Namen oder Pass-by-Wert. Darüber hinaus hat "Call-by-Name" nichts mit Namen zu tun . => Intist ein anderer Typ als Int; Es ist "Funktion ohne Argumente, die ein Int" gegen nur erzeugen Int. Sobald Sie über erstklassige Funktionen verfügen, müssen Sie keine Call-by-Name-Terminologie erfinden, um dies zu beschreiben.
Ben

2
@ Ben, das hilft bei der Beantwortung einiger Fragen, danke. Ich wünschte, mehr Aufzeichnungen würden die Semantik der Namensübergabe klar erklären.
Christopher Poile

3
@SelimOber Wenn der Text f(2)als Ausdruck vom Typ kompiliert wird Int, wird der generierte Code fmit Argument aufgerufen 2und das Ergebnis ist der Wert des Ausdrucks. Wenn derselbe Text als Ausdruck vom Typ kompiliert => Intwird, verwendet der generierte Code einen Verweis auf eine Art "Codeblock" als Wert des Ausdrucks. In beiden Fällen kann ein Wert dieses Typs an eine Funktion übergeben werden, die einen Parameter dieses Typs erwartet. Ich bin mir ziemlich sicher, dass Sie dies mit variabler Zuweisung tun können, ohne dass ein Parameter in Sicht ist. Was haben Namen oder Anrufe damit zu tun?
Ben

4
@Ben Also, wenn => Int"Funktion ohne Argumente, die ein Int erzeugt" ist, wie unterscheidet es sich von () => Int? Scala scheint diese unterschiedlich zu behandeln, zum Beispiel => Intfunktioniert es anscheinend nicht als Typ eines val, sondern nur als Typ eines Parameters.
Tim Goodman

5
@ TimGoodman Du hast recht, es ist ein bisschen komplizierter als ich herausgefunden habe. => Intist eine Annehmlichkeit und wird nicht genau so implementiert, wie es ein Funktionsobjekt ist (vermutlich, warum Sie keine Variablen vom Typ haben können => Int, obwohl es keinen fundamentalen Grund gibt, warum dies nicht funktionieren könnte). () => Intist explizit eine Funktion ohne Argumente, die ein zurückgeben Int, das explizit aufgerufen werden muss und als Funktion übergeben werden kann. => Intist eine Art "Proxy Int", und das einzige, was Sie damit tun können, ist es (implizit) aufzurufen, um das zu erhalten Int.
Ben

51

Hier ein Beispiel von Martin Odersky:

def test (x:Int, y: Int)= x*x

Wir wollen die Bewertungsstrategie untersuchen und feststellen, welche unter diesen Bedingungen schneller ist (weniger Schritte):

test (2,3)

Aufruf nach Wert: Test (2,3) -> 2 * 2 -> 4
Aufruf nach Name: Test (2,3) -> 2 * 2 -> 4
Hier wird das Ergebnis mit der gleichen Anzahl von Schritten erreicht.

test (3+4,8)

Anruf nach Wert: Test (7,8) -> 7 * 7 -> 49
Anruf nach Name: (3 + 4) (3 + 4) -> 7 (3 + 4) -> 7 * 7 -> 49
Hier anrufen nach Wert ist schneller.

test (7,2*4)

call by value: test (7,8) -> 7 * 7 -> 49
call by name: 7 * 7 -> 49
Hier ist call by name schneller

test (3+4, 2*4) 

Anruf nach Wert: Test (7,2 * 4) -> Test (7, 8) -> 7 * 7 -> 49
Anruf nach Name: (3 + 4) (3 + 4) -> 7 (3 + 4) -> 7 * 7 -> 49
Das Ergebnis wird in den gleichen Schritten erreicht.


1
Im dritten Beispiel für CBV meinten Sie Test (7,8) anstelle von Test (7,14)
Talonx

1
Das Beispiel stammt aus Coursera, dem Prinzip der Scala-Programmierung. Vorlesung 1.2. Der Aufruf nach Namen sollte lauten def test (x:Int, y: => Int) = x * x, dass der Parameter y niemals verwendet wird.
Dr.

1
Gutes Beispiel! Entnommen aus dem Coursera MOOC :)
Alxsimo

Dies ist eine gute Erklärung für den Unterschied, geht aber nicht auf die gestellte Frage ein, nämlich welche der beiden Scala anruft
db1234

16

In Ihrem Beispiel werden alle Parameter ausgewertet, bevor sie in der Funktion aufgerufen werden, da Sie sie nur nach Wert definieren . Wenn Sie Ihre Parameter nach Namen definieren möchten, sollten Sie einen Codeblock übergeben:

def f(x: => Int, y:Int) = x

Auf diese Weise wird der Parameter xerst ausgewertet, wenn er in der Funktion aufgerufen wird.

Dieser kleine Beitrag hier erklärt dies auch sehr gut.


10

Um @ Bens Punkt in den obigen Kommentaren zu wiederholen, denke ich, dass es am besten ist, sich "Call-by-Name" nur als syntaktischen Zucker vorzustellen. Der Parser verpackt die Ausdrücke nur in anonyme Funktionen, damit sie zu einem späteren Zeitpunkt aufgerufen werden können, wenn sie verwendet werden.

In der Tat, anstatt zu definieren

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

und läuft:

scala> callByName(something())
calling something
x1=1
calling something
x2=1

Sie könnten auch schreiben:

def callAlsoByName(x: () => Int) = {
  println("x1=" + x())
  println("x2=" + x())
}

Führen Sie es für den gleichen Effekt wie folgt aus:

callAlsoByName(() => {something()})

calling something
x1=1
calling something
x2=1

Ich denke du meintest: <! - Sprache: lang-scala -> def callAlsoByName (x: () => Int) = {println ("x1 =" + x ()) println ("x2 =" + x () ))} und dann: <! - language: lang-js -> callAlsoByName (() => etwas ()) Ich glaube nicht, dass Sie in diesem letzten Aufruf die geschweiften Klammern um etwas () benötigen. Hinweis: Ich habe versucht, nur Ihre Antwort zu bearbeiten, aber meine Bearbeitung wurde von Rezensenten abgelehnt, die sagten, es sollte sich stattdessen um einen Kommentar oder eine separate Antwort handeln.
Lambdista

Anscheinend können Sie in Kommentaren keine Syntaxhervorhebung verwenden. Ignorieren Sie einfach den Teil "<! - language: lang-scala ->"! Ich hätte meinen eigenen Kommentar bearbeitet, aber du darfst ihn nur innerhalb von 5 Minuten machen! :)
Lambdista

1
Ich bin kürzlich auch darauf gestoßen. Es ist in Ordnung, es konzeptionell so zu denken, aber Scala unterscheidet zwischen => Tund () => T. Eine Funktion, die den ersten Typ als Parameter verwendet, akzeptiert den zweiten nicht. Scala speichert genügend Informationen in @ScalaSignatureAnmerkungen, um einen Kompilierungszeitfehler dafür auszulösen. Der Bytecode für beide => Tund () => Tist jedoch gleich und ist a Function0. Weitere Informationen finden Sie in dieser Frage .
vsnyc

6

Ich werde versuchen, dies anhand eines einfachen Anwendungsfalls zu erklären, anstatt nur ein Beispiel zu liefern

Stellen Sie sich vor, Sie möchten eine "Nagger-App" erstellen , die Sie jedes Mal nervt , seit Sie das letzte Mal genagt wurden.

Untersuchen Sie die folgenden Implementierungen:

object main  {

    def main(args: Array[String]) {

        def onTime(time: Long) {
            while(time != time) println("Time to Nag!")
            println("no nags for you!")
        }

        def onRealtime(time: => Long) {
            while(time != time) println("Realtime Nagging executed!")
        }

        onTime(System.nanoTime())
        onRealtime(System.nanoTime())
    }
}

In der obigen Implementierung funktioniert der Nagger nur bei der Übergabe des Namens. Der Grund dafür ist, dass er beim Übergeben des Werts erneut verwendet wird und daher der Wert nicht neu bewertet wird, während bei der Übergabe des Namens der Wert jedes Mal neu bewertet wird Zeit, auf die auf die Variablen zugegriffen wird


4

In der Regel sind Parameter für Funktionen By-Value-Parameter. Das heißt, der Wert des Parameters wird bestimmt, bevor er an die Funktion übergeben wird. Was aber, wenn wir eine Funktion schreiben müssen, die einen Ausdruck als Parameter akzeptiert, den wir erst auswerten möchten, wenn er in unserer Funktion aufgerufen wird? Unter diesen Umständen bietet Scala Call-by-Name-Parameter an.

Ein Call-by-Name-Mechanismus übergibt einen Codeblock an den Angerufenen. Jedes Mal, wenn der Angerufene auf den Parameter zugreift, wird der Codeblock ausgeführt und der Wert berechnet.

object Test {
def main(args: Array[String]) {
    delayed(time());
}

def time() = {
  println("Getting time in nano seconds")
  System.nanoTime
}
def delayed( t: => Long ) = {
  println("In delayed method")
  println("Param: " + t)
  t
}
}
 1. C: /> scalac Test.scala 
 2. Scala-Test
 3. In verzögerter Methode
 4. Zeit in Nanosekunden bekommen
 5. Param: 81303808765843
 6. Zeit in Nanosekunden

2

Wie ich annehme, call-by-valueübergibt die oben diskutierte Funktion nur die Werte an die Funktion. Laut Martin OderskyIt handelt es sich um eine Evaluierungsstrategie, gefolgt von einer Scala, die die wichtige Rolle bei der Funktionsbewertung spielt. Aber machen Sie es einfach call-by-name. Es ist wie eine Übergabe der Funktion als Argument an die Methode auch bekannt als Higher-Order-Functions. Wenn die Methode auf den Wert des übergebenen Parameters zugreift, ruft sie die Implementierung übergebener Funktionen auf. wie nachstehend:

Erstellen Sie die Methode gemäß dem @ dhg-Beispiel zuerst wie folgt:

def something() = {
 println("calling something")
 1 // return value
}  

Diese Funktion enthält eine printlnAnweisung und gibt einen ganzzahligen Wert zurück. Erstellen Sie die Funktion, die Argumente als call-by-name:

def callByName(x: => Int) = {
 println("x1=" + x)
 println("x2=" + x)
}

Dieser Funktionsparameter definiert eine anonyme Funktion, die einen ganzzahligen Wert zurückgegeben hat. Darin xenthalten ist eine Definition der Funktion, die 0Argumente übergeben hat, aber einen Rückgabewert enthält int, und unsere somethingFunktion enthält dieselbe Signatur. Wenn wir die Funktion aufrufen, übergeben wir die Funktion als Argument an callByName. Aber im Falle call-by-valueseiner nur übergeben Sie den ganzzahligen Wert an die Funktion. Wir nennen die Funktion wie folgt:

scala> callByName(something())
 calling something
 x1=1
 calling something
 x2=1 

In dieser wird unsere somethingMethode zweimal aufgerufen, denn wenn wir auf den Wert von xin callByNamemethod zugreifen , ruft sie die Definition von somethingmethod auf.


2

Call by Value ist ein allgemeiner Anwendungsfall, wie in vielen Antworten hier erläutert.

Call-by-Name übergibt einen Codeblock an den Anrufer. Jedes Mal, wenn der Anrufer auf den Parameter zugreift, wird der Codeblock ausgeführt und der Wert berechnet.

Ich werde versuchen, den Anruf mit Namen anhand der folgenden Anwendungsfälle einfacher zu demonstrieren

Beispiel 1:

Ein einfaches Beispiel / ein Anwendungsfall für einen Aufruf nach Namen befindet sich unter der Funktion, die die Funktion als Parameter verwendet und die verstrichene Zeit angibt.

 /**
   * Executes some code block and prints to stdout the 
time taken to execute   the block 
for interactive testing and debugging.
   */
  def time[T](f: => T): T = {
    val start = System.nanoTime()
    val ret = f
    val end = System.nanoTime()

    println(s"Time taken: ${(end - start) / 1000 / 1000} ms")

    ret
  }

Beispiel 2:

Apache Spark (mit Scala) verwendet die Protokollierung mithilfe des Namensaufrufs (siehe LoggingMerkmal), bei dem träge bewertet wird, ob log.isInfoEnableddie unten beschriebene Methode verwendet wird oder nicht.

protected def logInfo(msg: => String) {
     if (log.isInfoEnabled) log.info(msg)
 }

2

Bei einem Call by Value wird der Wert des Ausdrucks zum Zeitpunkt des Funktionsaufrufs vorberechnet und dieser bestimmte Wert als Parameter an die entsprechende Funktion übergeben. Der gleiche Wert wird während der gesamten Funktion verwendet.

Während bei einem Call by Name der Ausdruck selbst als Parameter an die Funktion übergeben wird und nur innerhalb der Funktion berechnet wird, wenn dieser bestimmte Parameter aufgerufen wird.

Der Unterschied zwischen Call by Name und Call by Value in Scala lässt sich anhand des folgenden Beispiels besser verstehen:

Code-Auszug

object CallbyExample extends App {

  // function definition of call by value
  def CallbyValue(x: Long): Unit = {
    println("The current system time via CBV: " + x);
    println("The current system time via CBV " + x);
  }

  // function definition of call by name
  def CallbyName(x: => Long): Unit = {
    println("The current system time via CBN: " + x);
    println("The current system time via CBN: " + x);
  }

  // function call
  CallbyValue(System.nanoTime());
  println("\n")
  CallbyName(System.nanoTime());
}

Ausgabe

The current system time via CBV: 1153969332591521
The current system time via CBV 1153969332591521


The current system time via CBN: 1153969336749571
The current system time via CBN: 1153969336856589

Im obigen Codeausschnitt wird für den Funktionsaufruf CallbyValue (System.nanoTime ()) die System- Nanozeit vorberechnet und diesem vorberechneten Wert wurde ein Parameter an den Funktionsaufruf übergeben.

Im Funktionsaufruf CallbyName (System.nanoTime ()) wird jedoch der Ausdruck "System.nanoTime ())" selbst als Parameter an den Funktionsaufruf übergeben, und der Wert dieses Ausdrucks wird berechnet, wenn dieser Parameter innerhalb der Funktion verwendet wird .

Beachten Sie die Funktionsdefinition der CallbyName-Funktion, bei der ein => Symbol zwischen dem Parameter x und seinem Datentyp steht. Das dortige Symbol zeigt an, dass die Funktion nach Namenstyp aufgerufen wird.

Mit anderen Worten, die Funktionsargumente für den Aufruf nach Wert werden vor der Eingabe der Funktion einmal ausgewertet, die Funktionsargumente für den Aufruf nach Namen werden jedoch nur dann innerhalb der Funktion ausgewertet, wenn sie benötigt werden.

Hoffe das hilft!


2

Hier ist ein kurzes Beispiel, das ich codiert habe, um einem Kollegen von mir zu helfen, der gerade am Scala-Kurs teilnimmt. Was ich interessant fand, ist, dass Martin die zuvor in der Vorlesung vorgestellte && Frage nicht als Beispiel verwendet hat. Auf jeden Fall hoffe ich, dass dies hilft.

val start = Instant.now().toEpochMilli

val calc = (x: Boolean) => {
    Thread.sleep(3000)
    x
}


def callByValue(x: Boolean, y: Boolean): Boolean = {
    if (!x) x else y
}

def callByName(x: Boolean, y: => Boolean): Boolean = {
    if (!x) x else y
}

new Thread(() => {
    println("========================")
    println("Call by Value " + callByValue(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


new Thread(() => {
    println("========================")
    println("Call by Name " + callByName(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


Thread.sleep(5000)

Die Ausgabe des Codes lautet wie folgt:

========================
Call by Name false
Time 64ms
========================
Call by Value false
Time 3068ms
========================

1

Parameter werden normalerweise als Wert übergeben, was bedeutet, dass sie ausgewertet werden, bevor sie im Funktionskörper eingesetzt werden.

Sie können den Aufruf eines Parameters nach Namen erzwingen, indem Sie beim Definieren der Funktion den Doppelpfeil verwenden.

// first parameter will be call by value, second call by name, using `=>`
def returnOne(x: Int, y: => Int): Int = 1

// to demonstrate the benefits of call by name, create an infinite recursion
def loop(x: Int): Int = loop(x)

// will return one, since `loop(2)` is passed by name so no evaluated
returnOne(2, loop(2))

// will not terminate, since loop(2) will evaluate. 
returnOne(loop(2), 2) // -> returnOne(loop(2), 2) -> returnOne(loop(2), 2) -> ... 

1

Es gibt bereits viele fantastische Antworten auf diese Frage im Internet. Ich werde eine Zusammenstellung mehrerer Erklärungen und Beispiele schreiben, die ich zu diesem Thema gesammelt habe, für den Fall, dass jemand es hilfreich findet

EINFÜHRUNG

Call-by-Value (CBV)

In der Regel sind Parameter für Funktionen Call-by-Value-Parameter. Das heißt, die Parameter werden von links nach rechts ausgewertet, um ihren Wert zu bestimmen, bevor die Funktion selbst ausgewertet wird

def first(a: Int, b: Int): Int = a
first(3 + 4, 5 + 6) // will be reduced to first(7, 5 + 6), then first(7, 11), and then 7

Call-by-Name (CBN)

Aber was ist, wenn wir eine Funktion schreiben müssen, die einen Ausdruck als Parameter akzeptiert, den wir erst auswerten, wenn er in unserer Funktion aufgerufen wird? Unter diesen Umständen bietet Scala Call-by-Name-Parameter an. Dies bedeutet, dass der Parameter unverändert an die Funktion übergeben wird und seine Bewertung nach der Substitution erfolgt

def first1(a: Int, b: => Int): Int = a
first1(3 + 4, 5 + 6) // will be reduced to (3 + 4) and then to 7

Ein Call-by-Name-Mechanismus übergibt einen Codeblock an den Aufruf. Jedes Mal, wenn der Aufruf auf den Parameter zugreift, wird der Codeblock ausgeführt und der Wert berechnet. Im folgenden Beispiel wird verzögert eine Nachricht gedruckt, die zeigt, dass die Methode eingegeben wurde. Als nächstes druckt verzögert eine Nachricht mit ihrem Wert. Schließlich gibt verzögert 't' zurück:

 object Demo {
       def main(args: Array[String]) {
            delayed(time());
       }
    def time() = {
          println("Getting time in nano seconds")
          System.nanoTime
       }
       def delayed( t: => Long ) = {
          println("In delayed method")
          println("Param: " + t)
       }
    }

In verzögerter Methode
Zeit in
Nanosekunden abrufen Param: 2027245119786400

Vor- und Nachteile für jeden Fall

CBN: + Wird häufiger beendet * siehe oben stehende Terminierung * + Hat den Vorteil, dass ein Funktionsargument nicht ausgewertet wird, wenn der entsprechende Parameter bei der Auswertung des Funktionskörpers nicht verwendet wird. - Es ist langsamer, es werden mehr Klassen erstellt (dh das Programm nimmt länger zu laden) und es verbraucht mehr Speicher.

CBV: + Es ist oft exponentiell effizienter als CBN, da es diese wiederholte Neuberechnung von Argumentausdrücken vermeidet, die mit Namen aufgerufen werden. Es wertet jedes Funktionsargument nur einmal aus. + Es spielt viel besser mit imperativen Effekten und Nebenwirkungen, da Sie in der Regel viel besser wissen, wann Ausdrücke ausgewertet werden. -Es kann zu einer Schleife während der Parameterauswertung führen * siehe oben stehende Terminierung *

Was ist, wenn die Kündigung nicht garantiert ist?

- Wenn die CBV-Bewertung eines Ausdrucks e endet, endet auch die CBN-Bewertung von e. - Die andere Richtung ist nicht wahr

Beispiel für die Nichtbeendigung

def first(x:Int, y:Int)=x

Betrachten Sie zuerst den Ausdruck (1, Schleife)

CBN: first (1, loop) → 1 CBV: first (1, loop) → Argumente dieses Ausdrucks reduzieren. Da es sich um eine Schleife handelt, werden die Argumente unendlich reduziert. Es endet nicht

UNTERSCHIEDE IN JEDEM FALLVERHALTEN

Definieren wir einen Methodentest, der sein wird

Def test(x:Int, y:Int) = x * x  //for call-by-value
Def test(x: => Int, y: => Int) = x * x  //for call-by-name

Fall1 Test (2,3)

test(2,3)2*24

Da wir mit bereits ausgewerteten Argumenten beginnen, werden die gleichen Schritte für Call-by-Value und Call-by-Name ausgeführt

Fall2 Test (3 + 4,8)

call-by-value: test(3+4,8) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7 * (3+4)7 * 749

In diesem Fall führt Call-by-Value weniger Schritte aus

Fall3-Test (7, 2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (7)*(7)49

Wir vermeiden die unnötige Berechnung des zweiten Arguments

Fall4-Test (3 + 4, 2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7*(3+4)7*749

Anderer Ansatz

Nehmen wir zunächst an, wir haben eine Funktion mit Nebeneffekt. Diese Funktion druckt etwas aus und gibt dann ein Int zurück.

def something() = {
  println("calling something")
  1 // return value
}

Jetzt definieren wir zwei Funktionen, die genau dieselben Int-Argumente akzeptieren, mit der Ausnahme, dass eines das Argument in einem Call-by-Value-Stil (x: Int) und das andere in einem Call-by-Name-Stil (x: => Int).

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}
def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

Was passiert nun, wenn wir sie mit unserer Nebenwirkungsfunktion aufrufen?

scala> callByValue(something())
calling something
x1=1
x2=1
scala> callByName(something())
calling something
x1=1
calling something
x2=1

Sie sehen also, dass in der Call-by-Value-Version der Nebeneffekt des übergebenen Funktionsaufrufs (etwas ()) nur einmal aufgetreten ist. In der Call-by-Name-Version trat der Nebeneffekt jedoch zweimal auf.

Dies liegt daran, dass Call-by-Value-Funktionen den Wert des übergebenen Ausdrucks vor dem Aufrufen der Funktion berechnen und daher jedes Mal auf denselben Wert zugegriffen wird. Call-by-Name-Funktionen berechnen jedoch bei jedem Zugriff den Wert des übergebenen Ausdrucks neu.

BEISPIELE, WO ES BESSER IST, CALL-BY-NAME ZU VERWENDEN

Von: https://stackoverflow.com/a/19036068/1773841

Einfaches Leistungsbeispiel: Protokollierung.

Stellen wir uns eine Schnittstelle wie diese vor:

trait Logger {
  def info(msg: => String)
  def warn(msg: => String)
  def error(msg: => String)
}

Und dann so verwendet:

logger.info("Time spent on X: " + computeTimeSpent)

Wenn die info-Methode nichts unternimmt (weil beispielsweise die Protokollierungsstufe höher konfiguriert wurde), wird computeTimeSpent nie aufgerufen, was Zeit spart. Dies passiert häufig bei Protokollierern, bei denen häufig Manipulationen an Zeichenfolgen auftreten, die im Vergleich zu den zu protokollierenden Aufgaben teuer sein können.

Beispiel für die Richtigkeit: Logikoperatoren.

Sie haben wahrscheinlich Code wie diesen gesehen:

if (ref != null && ref.isSomething)

Stellen Sie sich vor, Sie würden die && Methode wie folgt deklarieren:

trait Boolean {
  def &&(other: Boolean): Boolean
}

Wenn ref dann null ist, wird eine Fehlermeldung angezeigt, da isSomething bei einer Nullreferenz aufgerufen wird, bevor es an && übergeben wird. Aus diesem Grund lautet die tatsächliche Erklärung:

trait Boolean {
  def &&(other: => Boolean): Boolean =
    if (this) this else other
}

1

Wenn Sie ein Beispiel durchgehen, können Sie den Unterschied besser verstehen.

Definieren wir eine einfache Funktion, die die aktuelle Zeit zurückgibt:

def getTime = System.currentTimeMillis

Jetzt definieren wir eine Funktion mit Namen , die zweimal verzögert um eine Sekunde gedruckt wird:

def getTimeByName(f: => Long) = { println(f); Thread.sleep(1000); println(f)}

Und eine Eins nach Wert :

def getTimeByValue(f: Long) = { println(f); Thread.sleep(1000); println(f)}

Nennen wir jetzt jeden:

getTimeByName(getTime)
// prints:
// 1514451008323
// 1514451009325

getTimeByValue(getTime)
// prints:
// 1514451024846
// 1514451024846

Das Ergebnis sollte den Unterschied erklären. Das Snippet finden Sie hier .


0

CallByNamewird bei Verwendung aufgerufen und callByValuebei jeder Begegnung mit der Anweisung aufgerufen.

Beispielsweise:-

Ich habe eine Endlosschleife, dh wenn Sie diese Funktion ausführen, werden wir nie scalaaufgefordert.

scala> def loop(x:Int) :Int = loop(x-1)
loop: (x: Int)Int

Eine callByNameFunktion verwendet die obige loopMethode als Argument und wird niemals in ihrem Körper verwendet.

scala> def callByName(x:Int,y: => Int)=x
callByName: (x: Int, y: => Int)Int

Bei der Ausführung der callByNameMethode finden wir kein Problem (wir erhalten eine scalaRückmeldung), da wir die Schleifenfunktion innerhalb der callByNameFunktion nicht verwenden können.

scala> callByName(1,loop(10))
res1: Int = 1
scala> 

Eine callByValueFunktion nimmt die obige loopMethode als Parameter, da das Ergebnis innerhalb der Funktion oder des Ausdrucks ausgewertet wird, bevor die äußere Funktion dort durch eine looprekursiv ausgeführte Funktion ausgeführt wird, und wir werden nie wieder scalaaufgefordert.

scala> def callByValue(x:Int,y:Int) = x
callByValue: (x: Int, y: Int)Int

scala> callByValue(1,loop(1))

0

Sieh dir das an:

    object NameVsVal extends App {

  def mul(x: Int, y: => Int) : Int = {
    println("mul")
    x * y
  }
  def add(x: Int, y: Int): Int = {
    println("add")
    x + y
  }
  println(mul(3, add(2, 1)))
}

y: => Int ist ein Aufruf mit Namen. Was als Namensaufruf übergeben wird, ist add (2, 1). Dies wird träge ausgewertet. Die Ausgabe auf der Konsole lautet also "mul", gefolgt von "add", obwohl add anscheinend zuerst aufgerufen wird. Call by Name fungiert als Übergabe eines Funktionszeigers.
Wechseln Sie nun von y: => Int zu y: Int. Die Konsole zeigt "add" gefolgt von "mul"! Übliche Art der Bewertung.


-2

Ich denke nicht, dass alle Antworten hier die richtige Rechtfertigung haben:

Beim Aufruf nach Wert werden die Argumente nur einmal berechnet:

def f(x : Int, y :Int) = x

// following the substitution model

f(12 + 3, 4 * 11)
f(15, 4194304)
15

Sie können oben sehen, dass alle Argumente ausgewertet werden, ob sie nicht benötigt werden. Normalerweise call-by-valuekönnen sie schnell sein, aber nicht immer wie in diesem Fall.

Wenn die Bewertungsstrategie call-by-namewäre, wäre die Zerlegung gewesen:

f(12 + 3, 4 * 11)
12 + 3
15

Wie Sie oben sehen können, mussten wir nie bewerten 4 * 11und haben daher ein wenig Berechnung gespeichert, was manchmal von Vorteil sein kann.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.